-
-
Notifications
You must be signed in to change notification settings - Fork 3
Description
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 match
ed 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/