Skip to main content

Exception Handling

Overview​

ExceptionHandler is a PartialFunction[Throwable, Route]. It intercepts exceptions thrown during route evaluation and converts them into HTTP responses. If no custom handler is provided, unhandled non-fatal exceptions result in a 500 Internal Server Error response.

Creating a Custom Handler​

Construct an ExceptionHandler by passing a partial function to ExceptionHandler.apply:

import pl.iterators.stir.server._
import pl.iterators.stir.server.Directives._
import org.http4s.Status

val myExceptionHandler = ExceptionHandler {
case _: ArithmeticException =>
complete(Status.BadRequest, "Arithmetic error")
case _: IllegalArgumentException =>
complete(Status.BadRequest, "Invalid argument")
}

The partial function matches on exception types and returns a Route that produces the desired error response.

Chaining Handlers with withFallback​

Combine two handlers so that the second acts as a fallback when the first does not match:

val combined = primaryHandler.withFallback(secondaryHandler)

If primaryHandler does not handle a given exception, secondaryHandler is consulted. A handler that has already been sealed (see below) will not accept a fallback, since it already handles all non-fatal exceptions.

Sealing a Handler​

The seal method attaches the default handler as a fallback, ensuring all non-fatal exceptions are covered:

val sealed = myExceptionHandler.seal()

An optional logAction parameter allows customizing how exceptions are logged:

import cats.effect.IO

val sealed = myExceptionHandler.seal(
logAction = Some((throwable, message) => IO.println(message))
)

Sealing an already sealed handler returns it unchanged, so a log action given the second time is ignored, as in Pekko. Route.seal takes no log action; to log through your own action, wrap the route in handleExceptions(ExceptionHandler.seal(handler)(logAction)) before sealing it.

Default Behavior​

The default ExceptionHandler (returned by ExceptionHandler.default()) handles all non-fatal exceptions by:

  1. IllegalRequestException(status, message) (raised for example by toStrictEntity when the entity does not arrive in time) completes with the given status and message.
  2. EntityStreamSizeException (raised by toStrictEntity and withSizeLimit when the entity is too large) completes with 413 Payload Too Large.
  3. Any other non-fatal exception is logged with its stack trace to stderr and completes with 500 Internal Server Error.

The handleExceptions Directive​

Apply a custom ExceptionHandler to an inner route using the handleExceptions directive from ExecutionDirectives:

val route =
handleExceptions(myExceptionHandler) {
path("divide") {
complete((1 / 0).toString)
}
}

Exceptions thrown during evaluation of the inner route -- whether thrown synchronously or produced by a failed IO -- are caught and processed by the provided handler. If the handler does not match the exception, it propagates outward.

Relationship with Route.seal​

Route.seal applies both a RejectionHandler and an ExceptionHandler to a route. It accepts an implicit ExceptionHandler parameter:

implicit val myHandler: ExceptionHandler = myExceptionHandler
val sealedRoute = Route.seal(route)

If no implicit ExceptionHandler is in scope, Route.seal uses the default handler. The ExceptionHandler.seal companion method handles null safely by falling back to ExceptionHandler.default().

When converting a route to an HttpApp[IO] via .toHttpApp, the route is automatically sealed with the default exception and rejection handlers.