Macro
A try/catch/finally macro for "forwards" error handling and finalization. Hello #9 and #10!
Source code: https://github.com/eutro/try-catch-match/blob/master/main.rkt
#lang racket/base
(provide try catch finally
try-with try-with*)
(require racket/match (for-syntax syntax/parse racket/base))
(begin-for-syntax
(define ((invalid-expr name) stx)
(raise-syntax-error name "invalid in expression context" stx)))
(define-syntax catch (invalid-expr 'catch))
(define-syntax finally (invalid-expr 'finally))
(begin-for-syntax
(define-syntax-class catch-clause
#:description "catch clause"
#:literals [catch]
(pattern (catch binding:expr body:expr ...+)))
(define-syntax-class finally-clause
#:description "finally clause"
#:literals [finally]
(pattern (finally body:expr ...+)))
(define-syntax-class body-expr
#:literals [catch finally]
(pattern (~and :expr
(~not (~or (finally . _)
(catch . _)))))))
(define-syntax (try stx)
(syntax-parse stx
[(_ body:body-expr ...+)
#'(let () body ...)]
[(_ body:body-expr ...+
catch:catch-clause ...
finally:finally-clause)
#'(call-with-continuation-barrier
(lambda ()
(dynamic-wind
void
(lambda ()
(try body ... catch ...))
(lambda ()
finally.body ...))))]
[(_ body:body-expr ...+
catch:catch-clause ...)
#'(with-handlers
([void
(lambda (e)
(match e
[catch.binding catch.body ...] ...
[_ (raise e)]))])
body ...)]))
(define-syntax (try-with stx)
(syntax-parse stx
[(_ ([name:id val:expr] ...)
body:body-expr ...+)
#'(let ([cust (make-custodian)])
(try
(define-values (name ...)
(parameterize ([current-custodian cust])
(values val ...)))
body ...
(finally (custodian-shutdown-all cust))))]))
(define-syntax (try-with* stx)
(syntax-parse stx
[(_ ([name:id val:expr] ...)
body:body-expr ...+)
#'(let ([cust (make-custodian)])
(try
(define-values (name ...)
(parameterize ([current-custodian cust])
(define name val) ...
(values name ...)))
body ...
(finally (custodian-shutdown-all cust))))]))
Documentation: https://docs.racket-lang.org/try-catch-match/index.html
try/catch/finally is a common and familiar syntax for handling exceptions, used in many languages such as Java, C++ and Clojure. Errors thrown within the try block may be "caught" by the catch clauses. In any case, whether by normal return or exception, the finally clause is executed.
The try macro achieves a similar result. Any exceptions thrown within the try expression's body will be matched against the catch clauses in succession, returning the result of the catch clause if the exception matches. Then, regardless of means, the finally clause is executed when leaving the dynamic extent of the try expression's body.
The expressiveness of match syntax makes it sufficiently flexible for any case, and grants familiarity to those that are used to it.
The try-with macro (and its cousin try-with*), influenced by with-open from Clojure and the try-with-resources from Java generalises resource cleanup in an exception-safe way.
Example
Occasionally with-handlers is unwieldy. Predicates and handlers have to be wrapped in functions, and the error handling code comes before the code that can cause the error. With try it can instead be declared after, without requiring explicit lambdas:
(try
(read port)
(catch (? exn:fail:read?) #f)
Perform cleanup such as decrementing a counter on exit:
(try
(increment-counter!)
(do-stuff)
(finally (decrement-counter!)))
Open a file and close it on exit:
(try-with ([port (open-output-file "file.txt")])
(displayln "Hello!" port))
Before and After
Exception-handling code is incredibly easy to get wrong. Typically it gets very little testing. with-handlers and dynamic-wind especially can be difficult to understand, and clunky to use. try/catch/finally presents a familiar syntax that is hopefully easy to use and leads to less bugs.
At the time of writing, R16 has two examples of erroneous code that could benefit from try:
This example currently doesn't handle exceptions properly.
A thread is notified and a counter incremented. A procedure that was passed in is executed, and the counter is decremented again.
However, the counter is not properly decremented for unexpected returns, such as exceptions.
(define (with-typing-indicator thunk)
(let ([payload (list client (hash-ref (current-message) 'channel_id))])
(thread-send typing-thread (cons 1 payload))
(let ([result (call-with-values thunk list)])
(thread-send typing-thread (cons -1 payload))
(apply values result))))
It could be rewritten with dynamic-wind, which may confuse those unfamiliar with it.
(define (with-typing-indicator thunk)
(let ([payload (list client (hash-ref (current-message) 'channel_id))])
(dynamic-wind
(lambda () (thread-send typing-thread (cons 1 payload)))
thunk
(lambda () (thread-send typing-thread (cons -1 payload))))))
Or with try:
(define (with-typing-indicator thunk)
(let ([payload (list client (hash-ref (current-message) 'channel_id))])
(thread-send typing-thread (cons 1 payload))
(try
(thunk)
(finally (thread-send typing-thread (cons -1 payload))))))
This example is a simple mistake made by the author, who forgot to wrap #f in const. A read exception thrown in this causes an error trying to apply #f.
(define (read-args)
(with-handlers ([exn:fail:read? #f])
(sequence->list (in-producer read eof (open-input-string args)))))
It could be rewritten with try, without requiring a const, as:
(define (read-args)
(try (sequence->list (in-producer read eof (open-input-string args)))
(catch (? exn:fail:read?) #f)))
Licence
This code is under the same MIT License that the Racket language uses. https://github.com/eutro/try-catch-match/blob/master/LICENSE
The associated text is licensed under the Creative Commons Attribution 4.0 International License http://creativecommons.org/licenses/by/4.0/
Macro
A
try/catch/finallymacro for "forwards" error handling and finalization. Hello #9 and #10!Source code: https://github.com/eutro/try-catch-match/blob/master/main.rkt
Documentation: https://docs.racket-lang.org/try-catch-match/index.html
try/catch/finallyis a common and familiar syntax for handling exceptions, used in many languages such as Java, C++ and Clojure. Errors thrown within thetryblock may be "caught" by thecatchclauses. In any case, whether by normal return or exception, thefinallyclause is executed.The
trymacro achieves a similar result. Any exceptions thrown within thetryexpression's body will bematched against thecatchclauses in succession, returning the result of thecatchclause if the exception matches. Then, regardless of means, thefinallyclause is executed when leaving the dynamic extent of thetryexpression's body.The expressiveness of
matchsyntax makes it sufficiently flexible for any case, and grants familiarity to those that are used to it.The
try-withmacro (and its cousintry-with*), influenced bywith-openfrom Clojure and thetry-with-resourcesfrom Java generalises resource cleanup in an exception-safe way.Example
Occasionally
with-handlersis unwieldy. Predicates and handlers have to be wrapped in functions, and the error handling code comes before the code that can cause the error. Withtryit can instead be declared after, without requiring explicit lambdas:Perform cleanup such as decrementing a counter on exit:
Open a file and close it on exit:
Before and After
Exception-handling code is incredibly easy to get wrong. Typically it gets very little testing.
with-handlersanddynamic-windespecially can be difficult to understand, and clunky to use.try/catch/finallypresents a familiar syntax that is hopefully easy to use and leads to less bugs.At the time of writing, R16 has two examples of erroneous code that could benefit from
try:This example currently doesn't handle exceptions properly.
A thread is notified and a counter incremented. A procedure that was passed in is executed, and the counter is decremented again.
However, the counter is not properly decremented for unexpected returns, such as exceptions.
It could be rewritten with
dynamic-wind, which may confuse those unfamiliar with it.Or with
try:This example is a simple mistake made by the author, who forgot to wrap
#finconst. A read exception thrown in this causes an error trying to apply#f.It could be rewritten with
try, without requiring aconst, as:Licence
This code is under the same MIT License that the Racket language uses. https://github.com/eutro/try-catch-match/blob/master/LICENSE
The associated text is licensed under the Creative Commons Attribution 4.0 International License http://creativecommons.org/licenses/by/4.0/