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:

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:

  1. 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.

  2. 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.

  3. 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.


1. Stylobate is the term for a supporter of columns in classic Greek and Roman architecture; here the interceptor is providing support to the rest of Pedestal.