This tutorial builds a domain exception and an ExceptionCatcher that turns it into a consistent JSON error response.
Define a domain exception#
Exceptions are plain Dart classes -- nothing framework-specific:
class NotFoundException implements Exception {
const NotFoundException(this.message);
final String message;
}
Catch it and shape the response#
A LifecycleComponent method that returns ExceptionCatcherResult<T> catches every exception of type
T thrown anywhere in the request lifecycle:
import 'package:revali_router/revali_router.dart';
class NotFoundCatcher implements LifecycleComponent {
const NotFoundCatcher();
ExceptionCatcherResult<NotFoundException> catchNotFound(
NotFoundException exception,
) {
return ExceptionCatcherResult.handled(
statusCode: 404,
body: {'error': exception.message},
);
}
}
The exception instance itself is bound automatically by matching the method's exception-typed parameter -- no annotation needed.
Throw it and register the catcher#
import 'package:revali_router/revali_router.dart';
@Controller('widgets')
class WidgetController {
const WidgetController();
@NotFoundCatcher()
@Get('missing')
String missing() {
throw const NotFoundException('Widget not found');
}
}
GET /widgets/missing now returns 404 with body {"error": "Widget not found"}, instead of an unhandled-exception
500.
Register @NotFoundCatcher() once at the app or controller level (instead of per-endpoint) to cover every route underneath it -- see
Scoping.
What's next?#
-
Authentication — block unauthorized requests with a
Guard - Exception Catchers reference — default catch-all, repetitive catchers, and non-JSON bodies
-
Error Responses — the full
statusCode/headers/bodyshape shared by Guards, Middleware, and Exception Catchers