An ExceptionCatcher is a Lifecycle Component that allows you to catch exceptions that are thrown during the request lifecycle. No matter where the exception is thrown, the request flow is aborted and the exception is caught by the server. The
ExceptionCatcher's responsibility is to handle certain types of exceptions and prepare an error response to be sent back to the client.
Recipes#
-
Prefer a domain exception → 4xx catcher for expected client failures (
ValidationException,UnauthorizedException, etc.). -
Do not use
ExceptionCatcher<Exception>as a typed catcher — the runtime requires a concrete subtype (see below). UseDefaultExceptionCatcheronly when you intentionally want a catch-all. -
Prefer the free
LifecycleComponentform (method returningExceptionCatcherResult<MyError>) for new code; classicextends ExceptionCatcher<T>remains supported. -
MissingArgumentException(missing/invalid@Query/@Body/… bindings) is mapped by the framework to HTTP 400 automatically — you do not need a custom catcher unless you want a different body shape.
Create an ExceptionCatcher#
To create an ExceptionCatcher, you need to extend the ExceptionCatcher class and implement the
catchException method. The catchException will only be called if the exception thrown is an instance of the type specified in the type argument of the
ExceptionCatcher class.
In this example, only exceptions of type MyException will be caught by the MyExceptionCatcher
class.
import 'package:revali_router/revali_router.dart';
final class MyExceptionCatcher extends ExceptionCatcher<MyException> {
const MyExceptionCatcher();
@override
ExceptionCatcherResult catchException(MyException exception, Context context) {
return const ExceptionCatcherResult.handled();
}
}
Register an ExceptionCatcher#
To register an ExceptionCatcher, annotate your MyExceptionCatcher class on the app, controller, or endpoint level.
import 'package:revali_router/revali_router.dart';
@App()
// highlight-next-line
@MyExceptionCatcher()
class MyApp ...
Register as Type Reference#
If you have a parameter that can not be provided at compile time, you can register the MyExceptionCatcher
as a type reference using the @Catchers() annotation.
import 'package:revali_router/revali_router.dart';
@App()
// highlight-next-line
@Catches([MyExceptionCatcher])
class MyApp ...
Learn more about type referencing.
Repetitive Catchers#
Its not common, but you can create multiple ExceptionCatcher classes that catch the same type of exception. This can be useful if you want to handle the same type of exception in different ways.
import 'package:revali_router/revali_router.dart';
final class MyOtherCatcher extends ExceptionCatcher<MyException> {
const MyOtherCatcher();
@override
ExceptionCatcherResult catchException(MyException exception, Context context) {
if (condition) {
return const ExceptionCatcherResult.handled();
} else {
return const ExceptionCatcherResult.unhandled();
}
}
}
When ExceptionCatcherResult.unhandled() is returned, the next ExceptionCatcher
that catches the same type of exception will be called.
Handling the Response#
The ExceptionCatcher is responsible for preparing the error response to be sent back to the client.
import 'package:revali_router/revali_router.dart';
final class MyExceptionCatcher extends ExceptionCatcher<MyException> {
const MyExceptionCatcher();
@override
ExceptionCatcherResult catchException(MyException exception, Context context) {
return const ExceptionCatcherResult.handled(
statusCode: 500,
headers: {
HttpHeaders.contentTypeHeader: 'text/plain',
}
body: 'An error occurred',
);
}
}
Here's an example of how you can handle the response:
const ExceptionCatcherResult.handled();
const ExceptionCatcherResult.unhandled(
statusCode: 500,
headers: {},
body: 'Internal Server Error',
);
Learn about returning error responses.
If the statusCode is not set, the default status code will be 500.
Default Exception Catcher#
If you would like to catch all exceptions that weren't caught by any other ExceptionCatcher, you can extend the
DefaultExceptionCatcher class and implement the catchException method. While you may be tempted to handle all exceptions in the default exception catcher, it is highly recommended to only handle exceptions that are not caught by any other
ExceptionCatcher.
import 'package:revali_router/revali_router.dart';
class UnhandledCatcher extends DefaultExceptionCatcher {
const UnhandledCatcher();
@override
ExceptionCatcherResult catchException(exception, context) {
return const ExceptionCatcherResult.handled();
}
}
There isn't a limit to the number of DefaultExceptionCatchers that can be created.
Scope the DefaultExceptionCatcher to the app level to catch all unhandled exceptions.
Unhandled Exceptions#
When an exception is not handled by any ExceptionCatcher, the default status code will be 500. The body will be set to the default error message.
Internal Server Error
Learn how you can customize the internal server error message in the docs
Learn more about type referencing.