When something bad—that is, an error—happens while a program is running, an exception is raised. “Raised” means the exception propagates upward through the chain of calling functions. If the exception isn’t caught by any of these functions, the program stops and the exception is printed, like so:
1 2 3
car: contract violation expected: pair? given: 42
Any kind of value can be used as an exception. Usually it’s an instance of the special exn structure, which carries information about the nature of the error and where it happened. In this case, car is raising an exception of type exn:fail:contract?.
We wrap with-handlers around an expression to catch exceptions that might arise from the expression. Below, the contract violation in car still raises a exn:fail:contract? exception. But this it time invokes a function designated to handle this exception. The error is suppressed, and we get the result of the function:
with-handlers is similar to cond. It contains a sequence of branches. On the left side of each branch is an exception ; on the right is a function that will handle the exception. Following these branches is a body consisting of any other expressions. The body is evaluated first. If an exception is raised within the body, with-handlers tests it against each left-hand predicate. If a match is found, it passes the exception to the handler on the right. If it doesn’t get a match, the exception exits the current with-handlers expression and continues propagating upward (where it might hit another with-handlers).
For convenience, Racket offers a set of built-in exception types that are organized as a hierarchy with inheritance. exn is at the top, then every exn:fail exception counts as an exn, then every exn:fail:contract exception also counts as an exn and an exn:fail, etc. Each exception type has a corresponding (spelled with a ? suffix, e.g. exn?, exn:fail?).
It’s a virtuous habit to construct exception predicates as narrowly as possible, to avoid false positives. For instance, (car 42) raises an exception of the type exn:fail:contract. We’re better off testing for that rather than the more generic exn:fail. Like cond, this lets us order our branches from particular to general:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
(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)) (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))) (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"))
1 2 3
'got-contract-exn 'got-zero-exn 'got-other-exn
The simplest way to raise an exception is with raise. Note that any value can be raised as an exception (and caught using with-handlers). It can, but does not have to be, one of Racket’s built-in exception types (like exn:fail):
1 2 3 4 5 6 7
When possible, it’s preferable to use one of the more specific variants of raise—like raise-argument-error, raise-result-error, or raise-syntax-error—because they automatically raise the conventional exception type for those errors, promoting better cooperation with handlers in the calling chain. For instance, this div function uses raise to emit its own special exception when it gets 0 for denom:
uncaught exception: 'holy-hell-about-your-zero-division
The problem is that no function in the calling chain is expecting this kind of exception, so there’s no handler to catch it (hence the “uncaught exception” message).
The better approach is to phrase the error in terms of raise-argument-error, which produces a standard contract-violation message:
1 2 3
div: contract violation expected: nonzero number given: 0
It will also be caught by a standard exn:fail:contract handler in the calling chain:
1 2 3 4 5 6 7
It’s wise to rely on Racket’s built-in exception types when we can, so that our exceptions cooperate with handlers in the wild (meaning, in other modules, especially ones we don’t control). For instance, if we were writing a math library, there would be no benefit to reinventing the exn:fail:contract:divide-by-zero exception type.
But other times, our error may not fit these generic types. Or it may be sufficiently special that we deliberately want to keep it from being caught by existing handlers. In those cases, we can create a new exception type with struct that inherits from an existing exception type (either the apex exn type or a subtype):
1 2 3 4 5 6 7 8 9 10 11 12
(struct exn:no-vowels exn:fail ()) ; subtype of `exn:fail` (define (raise-no-vowels-error word) (raise (exn:no-vowels (format "word ~v has no vowels" word) (current-continuation-marks)))) (define (f word) (unless (regexp-match #rx"[aeiou]" word) (raise-no-vowels-error word)) (displayln word)) (f "strtd")
word "strtd" has no vowels
We catch a custom exception the same way as a built-in exception:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
(struct exn:no-vowels exn:fail ()) ; subtype of `exn:fail` (define (raise-no-vowels-error word) (raise (exn:no-vowels (format "word ~v has no vowels" word) (current-continuation-marks)))) (define (f word) (unless (regexp-match #rx"[aeiou]" word) (raise-no-vowels-error word)) (displayln word)) (with-handlers ([exn:no-vowels? (λ (exn) 'no-vowels-special)]) (f "strtd")) (with-handlers ([exn:fail? (λ (exn) 'no-vowels-generic)]) (f "strtd"))
Though exceptions and handlers are typically associated with catching errors, they don’t have to be used that way. They can also be used to send & receive other kinds of signals, and thereby create alternative control flows.
For instance, Racket has no return statement. Usually it’s not missed. But sometimes it’s handy to be able to break out of a deeply nested chain of functions. We can use exceptions to simulate this control flow. A contrived example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
(struct exn:all-zero exn ()) (define (signal-all-even) (raise (exn:all-zero "all zero" (current-continuation-marks)))) (define (a) (b (random 10))) (define (b a-val) (c a-val (random 10))) (define (c a-val b-val) (when (andmap zero? (list a-val b-val (random 10))) (signal-all-even))) (with-handlers ([integer? (λ (trial) trial)]) (for ([trial (in-naturals)]) (with-handlers ([exn:all-zero? (λ (exn) (raise trial))]) (a))))
The function a picks a random integer from 0–9 inclusive and passes it to b, which does the same and passes the two integers to c, which does the same and ends up with a list of three random integers. The for loop will call a repeatedly until c has a list of three zeroes, at which point it will print the number of attempts it took, for instance:
1074 ; different every time, of course
We do this by creating a special exn:all-zero exception that acts as a signal that we’ve found a list of three zeroes. We raise this exception within c once we’ve found our target list. We catch this exception within the loop (using with-handlers as usual) and then raise trial as a new exception, which is the number of attempts we’ve made so far. Outside the loop, we catch this second exception and print it as a result. This is a perverse way of accomplishing this particular task. But it shows how we can rewire program flow with exceptions. + Compare: an exception lets us jump back through the chain of calling functions any distance. A return, by contrast, can only jump back to the immediately previous caller.
Within Racket, exceptions are members of a broader class of things called continuations that can rewire program flow in more arbitrary ways.