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.

This function is provided in the io.pedestal/pedestal.error library; to use error-dispatch, you must explicitly include the library in your project.

Error dispatch uses core.match to match the ex-data of the exception; this allows you to handle a variety of different scenarios very concisely. core.match works top-to-bottom, so you order your most specific cases first, and your more general cases later.

Each match clauses specifies certain keys from the above list, and corresponding values to match against; the first match term that satisified all the conditions is executed, which determines the behavior when a matching error occurs.

Here is an example from the test suite:

(def service-error-handler (1)
  (error-dispatch [context ex]
    (2)
    [{:exception-type :java.lang.ArithmeticException :interceptor ::another-bad-one}]
    (assoc context :response {:status 400 :body "Another bad one"})

    (3)
    [{:exception-type :java.lang.ArithmeticException}]
    (assoc context :response {:status 400 :body "A bad one"})

    (4)
    :else
    (assoc context :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 with a :leave callback. Binding the interceptor to a var is the normal case.
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.

Logging Uncaught Exceptions

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.

Some exceptions should not be logged; by default, Pedestal will identify an exception that indicates a broken pipe to the client when sending the response; such errors will not be logged but will be quietly ignored.

This logic can be altered using the service map :io.pedestal.http/service-fn-options key; the value is itself a map, and the key :exception-analyzer can be specified.

An exception analyzer function is passed the context map and the unhandled exception; it may return nil to prevent the exception from being logged, or return the exception itself, or an alternate exception, to have that be the exception that is logged.

When not specified, the default-exception-analyzer function is used; this is where the logic to check for a broken pipe exception lives.


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.