Fork me on GitHub

In Application class you can set a custom ErrorHandler.
ErrorHandler is the core ExceptionHandler. ErrorHandler can register custom ExceptionHandlers and will handle an exception with a matching ExceptionHandler, if found. Otherwise ErrorHandler will fallback to itself to handle the exception.
By default each Application instance comes with a DefaultErrorHandler that integrates with the TemplateEngine & ContentTypeEngine. It generates a representation of an exception or error result.

See below an custom ErrorHandler in action:

public class BasicApplication extends Application {

    @Override
    protected void onInit() {
        // throw a programmatically exception
        GET("/exception", routeContext -> {
            throw new RuntimeException("My programmatically error");
        });

        // throw an exception that gets handled by a registered ExceptionHandler
        GET("/whoops", routeContext -> {
            throw new ForbiddenException("You didn't say the magic word!");
        });

        // register a custom ExceptionHandler
        getErrorHandler().setExceptionHandler(ForbiddenException.class, new ExceptionHandler() {
        
            @Override
            public void handle(Exception e, RouteContext routeContext) {
                routeContext.setLocal("message", e.getMessage());
                // render the template associated with this http status code ("pippo/403forbidden" by default)
                getErrorHandler().handle(403, routeContext);
            }
            
        });
    }
    
    public static class ForbiddenException extends RuntimeException {

        public ForbiddenException(String message) {
            super(message);
        }
        
    }

}

If you want to customize the template content for the default error templates (see the list of templates names in TemplateEngine) then create a template with the same name in src/main/resources/templates folder.

So, by default, the following templates are available (copy/paste from TemplateEngine.java):

public final static String BAD_REQUEST_400 = "pippo/400badRequest";
public final static String UNAUTHORIZED_401 = "pippo/401unauthorized";
public final static String PAYMENT_REQUIRED_402 = "pippo/402paymentRequired";
public final static String FORBIDDEN_403 = "pippo/403forbidden";
public final static String NOT_FOUND_404 = "pippo/404notFound";
public final static String METHOD_NOT_ALLOWED_405 = "pippo/405methodNotAllowed";
public final static String CONFLICT_409 = "pippo/409conflict";
public final static String GONE_410 = "pippo/410gone";
public final static String INTERNAL_ERROR_500 = "pippo/500internalError";
public final static String NOT_IMPLEMENTED_501 = "pippo/501notImplemented";
public final static String OVERLOADED_502 = "pippo/502overloaded";
public final static String SERVICE_UNAVAILABLE_503 = "pippo/503serviceUnavailable";

Each template engine implementation comes with above templates. For example if we take a look at pippo-template-parent/pippo-freemarker/src/main/resources we can see:

$ tree .
.
└── templates
    └── pippo
        ├── 000base.ftl
        ├── 400badRequest.ftl
        ├── 401unauthorized.ftl
        ├── 402paymentRequired.ftl
        ├── 403forbidden.ftl
        ├── 404notFound.ftl
        ├── 405methodNotAllowed.ftl
        ├── 409conflict.ftl
        ├── 410gone.ftl
        ├── 500internalError.ftl
        ├── 501notImplemented.ftl
        ├── 502overloaded.ftl
        └── 503serviceUnavailable.ftl

If you use FreemarkerTemplateEngine in your application and you want to replace/override the content of 404notFound.ftl template, then place your variant of 404notFound.ftl in your src/main/resources/templates/pippo directory.

If you are not happy with this fine granularity (a template for each HTTP error code), then you can create your custom ErrorHandler that returns the same template for any status code:

public class MyErrorHandler extends DefaultErrorHandler {
    
    public ExampleErrorHandler(Application application) {
        super(application);
    }

    @Override
    protected String getTemplateForStatusCode(int statusCode) {
        return "error.ftl"; // or simple "error" without extension
    }
    
}

and use your custom error handler in your application:

public class MyApplication extends Application {
    
    @Override
    protected void onInit() {
        setErrorHandler(new MyErrorHandler(this));
        
        // add routes below
    }
    
}