try-make-sarna-happy
1 The try macro
try
catch
catch/  match
finally
1.1 Examples
1.2 Before and After
1.3 Implementation
2 License and Acknowledgements
8.2

try-make-sarna-happy

D. Ben Knoble

 (require try-make-sarna-happy)
  package: try-make-sarna-happy

This package was written for the 2021 Syntax Parse bee and to satisfy sarna’s complaints on Discord with with-handlers, which leads to a "backwards" looking program:

; say first what to do with the exception
(with-handlers ([exn:fail:syntax?
                   (λ (e) (displayln "got a syntax error"))])
  ; and only then what to actually do
  (raise-syntax-error #f "a syntax error"))

(from the with-handlers examples).

There are "competing" versions of this macro, so choose your style:

1 The try macro

syntax

(try body-expr ...+ maybe-catch maybe-catch/match maybe-finally)

 
maybe-catch = 
  | (catch [pred-expr exn-id handler-expr ...+] ...)
     
maybe-catch = 
  | (catch/match [match-expr handler-expr ...+] ...)
     
maybe-finally = 
  | (finally finally-expr ...+)
Tries body-exprs in order, returning the value of the last. If an exception is raised, it may be handled by one of the catch clauses, whose value becomes the value of the overall form. The optional finally clause is always run.

The catch clauses use with-handlers, but in a different format: when pred-expr returns true for a thrown exception, exn-id is bound to the exception for the body handler-expr.

The catch/match clauses are match forms tested against the exception.

When both catch-style and catch/match-style clauses are present, all of the catch-style clauses are tried before any of the catch/match clauses.

syntax

(catch [pred-expr exn-id handler-expr ...+])

Used in try to specify exception handlers. An exception is tested against each pred-expr; if the test succeeds, the exception is bound to exn-id and handler-expr runs.

If no pred-exprs succeed, testing proceeds to any catch/match clauses.

syntax

(catch/match [match-expr handler-expr ...+])

Used in try to specify exception handlers. An exception is matched (in the sense of match) against match-expr; if the match succeeds handler-expr runs.

If no match-exprs succeed, the exception is re-raised.

syntax

(finally finally-expr ...+)

Used in try to specify a body of finally-exprs which run when the try-body exits, be it through a return, continuation jump, or exception.

1.1 Examples

Examples:
> (try
    (/ 10 0)
    (catch [exn? e (exn-message e)]))

"/: division by zero"

> (struct my-error [x y])
> (try
    (raise (my-error 1 2))
    (catch/match [(my-error 1 y) y]))

2

> (let ([resource (get-handle)])
    (try
      (use-might-break resource)
      (catch [exn? e (displayln (exn-message e))])
      (finally
        (close resource)))
    (is-closed? resource))

use-might-break: something went wrong

#t

1.2 Before and After

This is a "Code Cleaning" macro: it tidies up a common pattern and makes it read in a forward direction. In this it is similar to the threading library but for exceptions.

The following Before/After pairs do not show the call-with-continuation-barrier call because they assumes none of the shown procedures muck with continuations. It is needed in the general case, however, to prevent a captured continuation from re-entering the dynamic-wind and thus causing the finally clause to be run more than once.

Before
> (with-handlers ([exn:fail:syntax?
                    (λ (e)
                      (displayln "got a syntax error"))])
    (raise-syntax-error #f "a syntax error"))

got a syntax error

After
> (try
    (raise-syntax-error #f "a syntax error")
    (catch [exn:fail:syntax? e
            (displayln "got a syntax error")]))

got a syntax error

Before
> (let ([resource (get-handle)])
    (dynamic-wind
      void
      (λ () (with-handlers ([exn? (λ (e) (displayln (exn-message e)))])
                           (use-might-break resource)))
      (λ () (close resource)))
    (is-closed? resource))

use-might-break: something went wrong

#t

After
> (let ([resource (get-handle)])
    (try
      (use-might-break resource)
      (catch [exn? e (displayln (exn-message e))])
      (finally
        (close resource)))
    (is-closed? resource))

use-might-break: something went wrong

#t

The following are from Beautiful Racket: Errors and Exceptions.

Before
> (with-handlers ([exn:fail:contract:divide-by-zero?
                    (λ (exn) 'got-zero-exn)]
                 [exn:fail:contract? (λ (exn) 'got-contract-exn)]
                 [exn:fail? (λ (exn) 'got-other-exn)])
   (car 42))

'got-contract-exn

After
> (try
    (car 42)
    (catch [exn:fail:contract:divide-by-zero? exn 'got-zero-exn]
           [exn:fail:contract? exn 'got-contract-exn]
           [exn:fail? exn 'got-other-exn]))

'got-contract-exn

Before
> (with-handlers ([exn:fail:contract:divide-by-zero?
                    (λ (exn) 'got-zero-exn)]
                  [exn:fail:contract? (λ (exn) 'got-contract-exn)]
                  [exn:fail? (λ (exn) 'got-other-exn)])
    (car (/ 42 0)))

'got-zero-exn

After
> (try
    (car (/ 42 0))
    (catch [exn:fail:contract:divide-by-zero? exn 'got-zero-exn]
           [exn:fail:contract? exn 'got-contract-exn]
           [exn:fail? exn 'got-other-exn]))

'got-zero-exn

Before
> (with-handlers ([exn:fail:contract:divide-by-zero?
                    (λ (exn) 'got-zero-exn)]
                  [exn:fail:contract? (λ (exn) 'got-contract-exn)]
                  [exn:fail? (λ (exn) 'got-other-exn)])
    (error "boom"))

'got-other-exn

After
> (try
    (error "boom")
    (catch [exn:fail:contract:divide-by-zero? exn 'got-zero-exn]
           [exn:fail:contract? exn 'got-contract-exn]
           [exn:fail? exn 'got-other-exn]))

'got-other-exn

1.3 Implementation

The following is provided explicitly for the 2021 Syntax Parse bee.

  #lang racket/base
   
  (provide try catch catch/match finally)
   
  (require (for-syntax racket/base)
           racket/match
           syntax/parse/define)
   
  (begin-for-syntax
    (define ((only-in-try name) stx)
      (raise-syntax-error name "not allowed except in try" stx)))
   
  (define-syntax catch (only-in-try 'catch))
  (define-syntax catch/match (only-in-try 'catch/match))
  (define-syntax finally (only-in-try 'finally))
   
  (begin-for-syntax
    (define-syntax-class try-body
      #:literals (catch catch/match finally)
      (pattern {~and :expr {~not {~or (catch . _) (catch/match . _) (finally . _)}}}))
   
    (define-syntax-class catch-clause
      #:attributes ((handlers 1))
      #:literals (catch)
      (pattern (catch [pred:expr name:id body:expr ...+] ...)
               #:with (handlers ...) #'([pred (λ (name) body ...)] ...)))
   
    ;; this one's for you, notjack
    (define-syntax-class catch-match-clause
      #:attributes (handler)
      #:literals (catch/match)
      (pattern (catch/match [clause:expr body:expr ...+] ...)
               #:with (match-clauses ...) #'([clause body ...] ...)
               #:with handler #'[(λ (_) #t) ;; catch 'em all
                                 (match-lambda
                                   match-clauses ...
                                   ;; rethrow as last resort
                                   [e (raise e)])]))
   
    (define-syntax-class finally-clause
      #:attributes (handler)
      #:literals (finally)
      (pattern (finally body:expr ...+)
               #:with handler #'(λ () body ...))))
   
  ;; Calls value-thunk, then post-thunk, with post-thunk guaranteed to be run
  ;; even if execution exits value-thunk through an exception or continuation
  ;;
  ;; value-thunk is prevented from re-entry and continutation shenanigans by a
  ;; continuation-barrier
  ;;
  ;; thanks to Alex Knauth & SamPh on Discord
  (define (call-with-try-finally value-thunk post-thunk)
    (call-with-continuation-barrier
      (λ () (dynamic-wind void value-thunk post-thunk))))
   
  (define-syntax-parser try
    [(_ body:try-body ...+
        {~optional c:catch-clause}
        {~optional m:catch-match-clause}
        {~optional f:finally-clause})
     #'(call-with-try-finally
         (λ ()
           (with-handlers ((~? (~@ c.handlers ...))
                           (~? m.handler))
             body ...))
         (~? f.handler void))])
   
  (module+ test
    (require racket
             rackunit)
   
    (check-equal?
      (try 1)
      1)
   
    (check-equal?
      (try (/ 1 0)
           (catch [exn:fail? e (exn-message e)]))
      "/: division by zero")
   
    (check-equal?
      (with-output-to-string
        (thunk
          (check-equal?
            (try 1
                 (finally (displayln "cleaning up")))
            1)))
      "cleaning up\n")
   
    (check-equal?
      (with-output-to-string
        (thunk
          (check-equal?
            (try (/ 1 0)
                 (catch [exn:fail? _ 0])
                 (finally (displayln "cleaning up")))
            0)))
      "cleaning up\n")
   
    (check-equal?
      (try (/ 1 0)
           (catch/match [(? exn:fail? e) (exn-message e)]))
      "/: division by zero")
   
    (struct posn [x y])
    (check-equal?
      (try (raise (posn 1 2))
           (catch/match [(posn 1 y) y]))
      2)
    (check-equal?
      (try (raise (posn 1 2))
           (catch [exn? e (exn-message e)])
           (catch/match [(posn 1 y) y]))
      2)
   
    (check-equal?
      (with-output-to-string
        (thunk
          (check-equal?
            (try 1
                 (finally (displayln "cleaning up")))
            1)))
      "cleaning up\n")
   
    (check-equal?
      (with-output-to-string
        (thunk
          (check-equal?
            (try (/ 1 0)
                 (catch/match [(? exn:fail?) 0])
                 (finally (displayln "cleaning up")))
            0)))
      "cleaning up\n"))
   

2 License and Acknowledgements

The code is licensed with the MIT license of the Racket project. The text of this documentation is licensed with the CCA 4.0 International License.

Thanks to