Time for an introduction to Undertow's ExceptionHandler interfaces and models. Oh wait, the ExceptionHandler is just a HttpHandler and it accepts HttpHandlers for each Exception type. Alright let's just jump right into to it then.

Custom Exceptions

Assuming we have the following custom exception classes.

public static class WebException extends RuntimeException {
    private final int statusCode;
    public WebException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }

    public int getStatusCode() {
        return statusCode;
    }
}

public static class ApiException extends WebException {
    public ApiException(int statusCode, String message) {
        super(statusCode, message);
    }
}

HttpHandlers

Implementations of each route.

public static void throwWebException(HttpServerExchange exchange) {
    throw new WebException(500, "Web Server Error");
}

public static void throwApiException(HttpServerExchange exchange) {
    throw new ApiException(503, "API Server Error");
}

public static void throwException(HttpServerExchange exchange) {
    throw new RuntimeException();
}

public static void ok(HttpServerExchange exchange) {
    Exchange.body().sendText(exchange, "ok");
}

Routing

Assign a route to each of the above HttpHandlers.

private static final HttpHandler ROUTES = new RoutingHandler()
    .get("/throwWebException", ExceptionHandlers::throwWebException)
    .get("/throwApiException", ExceptionHandlers::throwApiException)
    .get("/throwException", ExceptionHandlers::throwException)
    .get("/ok", ExceptionHandlers::ok)
;

Exception Handlers

This should be straightforward, notice an ExceptionHandler is no different than a HttpHandler. We utilize undertows attachment system to grab the Exception.

public static void handleWebException(HttpServerExchange exchange) {
    WebException ex = (WebException) exchange.getAttachment(ExceptionHandler.THROWABLE);
    exchange.setStatusCode(ex.getStatusCode());
    Exchange.body().sendHtml(exchange, "<h1>" + ex.getMessage() + "</h1>");
}

public static void handleApiException(HttpServerExchange exchange) {
    ApiException ex = (ApiException) exchange.getAttachment(ExceptionHandler.THROWABLE);
    exchange.setStatusCode(ex.getStatusCode());
    Exchange.body().sendJson(exchange, "{\"message\": \"" + ex.getMessage() + "\"}");
}

public static void handleAllExceptions(HttpServerExchange exchange) {
    exchange.setStatusCode(500);
    Exchange.body().sendText(exchange, "Internal Server Error!");
}

Exception Handler

Exception handling is easy in undertow. Just create an ExceptionHandler instance and add a HttpHandler for each Exception type you wish to handle. Make sure you main route is passed into the ExceptionHandler. This is how we compose more complex routes.

/*
 * The Exception handler wraps the routing handler.
 * Undertow allows as little or as much flexibility as you
 * need through this style of composition.
 */
private static final HttpHandler ROOT = Handlers.exceptionHandler(ROUTES)
    /*
     * The order here is import with exception subclassing.
     * Add the most restrictive classes first. If we put
     * Throwable first it would handle all of the exceptions.
     */
    .addExceptionHandler(ApiException.class, ExceptionHandlers::handleApiException)
    .addExceptionHandler(WebException.class, ExceptionHandlers::handleWebException)
    .addExceptionHandler(Throwable.class, ExceptionHandlers::handleAllExceptions)
;

Finally the Server

public static void main(String[] args) {
    // Notice we pass ROOT here not ROUTES.
    SimpleServer server = SimpleServer.simpleServer(ROOT);
    server.start();
}

Results

curl -v localhost:8080/ok
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /ok HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain
< Content-Length: 2
< Date: Wed, 11 Jan 2017 01:37:47 GMT
<
* Connection #0 to host localhost left intact
ok
curl -v localhost:8080/throwWebException
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /throwWebException HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Connection: keep-alive
< Content-Type: text/html
< Content-Length: 25
< Date: Wed, 11 Jan 2017 01:38:34 GMT
<
* Connection #0 to host localhost left intact
<h1>Web Server Error</h1>
curl -v localhost:8080/throwApiException
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /throwApiException HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 503 Service Unavailable
< Connection: keep-alive
< Content-Type: application/json
< Content-Length: 31
< Date: Wed, 11 Jan 2017 01:38:58 GMT
<
* Connection #0 to host localhost left intact
{"message": "API Server Error"}
curl -v localhost:8080/throwException
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /throwException HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Connection: keep-alive
< Content-Type: text/plain
< Content-Length: 22
< Date: Wed, 11 Jan 2017 01:40:20 GMT
<
* Connection #0 to host localhost left intact
Internal Server Error!

Notice all status codes and content types are correct! No magic involved, no injecting state. An added benefit to this approach is how manageable the stack traces are. For more info read up on managing your stack traces