LogoRevali

Exception Catchers

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). Use DefaultExceptionCatcher only when you intentionally want a catch-all.
  • Prefer the free LifecycleComponent form (method returning ExceptionCatcherResult<MyError>) for new code; classic extends 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.

lib/components/catchers/my_catcher.dart
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.

routes/my_app.dart
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.

routes/my_app.dart
import 'package:revali_router/revali_router.dart';

@App()
// highlight-next-line
@Catches([MyExceptionCatcher])
class MyApp ...

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.

lib/components/catchers/my_other_catcher.dart
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.

lib/components/catchers/my_catcher.dart
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',
);

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.

lib/components/catchers/unhandled_catcher.dart
import 'package:revali_router/revali_router.dart';

class UnhandledCatcher extends DefaultExceptionCatcher {
    const UnhandledCatcher();

    @override
    ExceptionCatcherResult catchException(exception, context) {
        return const ExceptionCatcherResult.handled();
    }
}

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 more about type referencing.