Error Handling
Pedestal must do something useful when an interceptor throws an exception. It would not be useful to bubble up the runtime call stack, because that call stack only includes the Pedestal machinery itself. Interceptors are data structures that conceptually represent frames on a call stack, but they are not present in the usual nested fashion on the Java call stack.
Remember that request processing may be asynchronous, in which case the call stack doesn’t even represent the flow of control to the point where the exception occurred.
Instead, Pedestal works backwards, looking for an interceptor to handle
the exception. All exceptions thrown from an
interceptor are captured; Pedestal wraps such exceptions in an ExceptionInfo
instance
which is then bound to the :io.pedestal.interceptor.chain/error key
in the context map.
As long as there is an error attached to that key, Pedestal will not invoke the usual :enter and :leave functions. Instead, it looks for the next interceptor in the chain that has an :error function attached to it.
This error handling proceeds the same way regardless of whether interceptors in the chain returned a context map or async channel. (See Interceptor Return Values for details on async return.) As a result, Pedestal unifies error handling for synchronous interceptors and asynchronous interceptors.
You can supply your own :error handling interceptor anywhere in the interceptor queue.
Error Function
Pedestal will call the function bound to :error on an interceptor with two arguments:
-
The context map
-
An
ExceptionInfo
instance
Pedestal wraps all exceptions in ex-info. The following keys are defined in its exception data map:
Key | Type | Description |
---|---|---|
:execution-id |
long |
Unique value that identifies one "run" through the interceptors. |
:stage |
Keyword |
Either :enter or :leave depending on which direction the execution was going. |
:interceptor |
Keyword |
The :name from the interceptor that threw the exception |
:exception-type |
Keyword |
Keywordized form of the exception’s Java class name |
:exception |
Throwable |
The caught exception. |
The context map does not contain the :io.pedestal.interceptor.chain/error key. The :error function can do one of the following things:
-
Return the context map. This is "catching" the error. Because the context map has no error bound to it, Pedestal will exit error handling and execute any remaining :leave handlers.
-
Return the context map with the exception re-attached at :io.pedestal.interceptor.chain/error. This indicates that the handler doesn’t recognize that exception and declines to handle it. Pedestal will continue looking for a handler.
-
Throw a new exception. This indicates that the interceptor should have been able to handle the error, but something went wrong. Pedestal will start looking for a handler for this new exception.
In that last case, Pedestal keeps track of the exceptions that were overridden. These are in a seq bound to :io.pedestal.interceptor.chain/suppressed in the context map.
Most commonly, the interceptor will return the context map with a :response map; this will trigger normal response processing by invoking any remaining interceptors' :leave functions.
Error Dispatch Interceptor
As a convenience, Pedestal offers a macro to build error-handling interceptors that use pattern-matching to dispatch.
error-dispatch
builds interceptors that use
core.match to select a clause.
Here is an example from the test suite:
(def service-error-handler (1)
(error-dispatch [ctx ex]
(2)
[{:exception-type :java.lang.ArithmeticException :interceptor ::another-bad-one}]
(assoc ctx :response {:status 400 :body "Another bad one"})
(3)
[{:exception-type :java.lang.ArithmeticException}]
(assoc ctx :response {:status 400 :body "A bad one"})
(4)
:else
(assoc ctx :io.pedestal.interceptor.chain/error ex)))
,,,
(def routes
(route/expand-routes
#{["/v1/api" :any [service-error-handler ,,,]]})) (5)
1 | This macro returns code that evaluates to an interceptor. Binding it to a var is the normal use. |
2 | This is a pattern match that looks for an ArithmeticException , but only thrown by a particular interceptor. If the pattern matches, this expression gets evaluated. The error is handled and a response attached. |
3 | Another pattern match expression, matching any ArithmeticException s that weren’t matched before. We return a different response body in these cases. |
4 | :else always matches because it is a truthy value. Reattaching the error tells Pedestal to keep looking for a handler. |
5 | We use the resulting interceptor like any other. |
Pattern matching is a concise way to express conditions for handling an error.
Error Handling and Logging
For HTTP, the Servlet Interceptor provides the "last ditch" error handlers that log the error and return an error response. In development mode, they emit a stack trace as the response body; you will see the error logged from the :io.pedestal.http.impl.servlet-interceptor/stylobate [1] interceptor.
The error logged by the "last ditch" error handler includes the context map. This is often quite large and may contain data you do not wish to log. |
While having defaults is nice, it is highly recommended that services implement their own error handling/logging. This gives you full control over how errors are processed.