Handle errors
You’ll make an API return the right status and a stable machine-readable code for each failure: not found, conflicts, business-rule violations, database constraint errors and unexpected crashes, all in one JSON shape.
Prerequisites
Section titled “Prerequisites”- A gisila project with services.
1. Throw HttpExceptions from services
Section titled “1. Throw HttpExceptions from services”import 'package:gisila/gisila.dart';
class AccountService extends Service { Future<Account> get(String id) async => await _repo.find(id) ?? (throw NotFound('Account $id not found'));
Future<void> withdraw(String id, int cents) async { final account = await get(id); if (account.balance < cents) { throw HttpException(422, 'Insufficient funds', code: 'insufficient_funds', details: {'balance': account.balance}); } if (account.frozen) throw Forbidden('Account is frozen'); await _repo.debit(id, cents); }}The client receives:
{"error":{"status":422,"message":"Insufficient funds","code":"insufficient_funds","details":{"balance":1200}}}Pick codes your clients can branch on, document them, and never change them. Named subclasses cover the common statuses: BadRequest, Unauthorized, Forbidden, NotFound, Conflict, PayloadTooLarge, TooManyRequests, InternalServerError.
2. Define your own exception types
Section titled “2. Define your own exception types”Subclass HttpException for errors you throw in many places:
class InsufficientFunds extends HttpException { InsufficientFunds(int balance) : super(422, 'Insufficient funds', code: 'insufficient_funds', details: {'balance': balance});}To change the body shape for one exception type, override toJson(). For a body that must match an external contract, throw EndpointException(statusCode: ..., body: {...}).
3. Map database errors once
Section titled “3. Map database errors once”Instead of catching constraint violations in every service, set a DbErrorMapper. With gisila_orm, use the bundled Postgres mapper:
import 'package:gisila_orm/web.dart';
AppConfig( database: GisilaOrmDatabaseProvider(db), dbErrorMapper: const PostgresDbErrorMapper(),)A unique violation then answers 409 with code unique_violation and "email" is already taken, naming the column but never the value. For other drivers, implement DbErrorMapper.map and return null for errors you don’t recognise.
4. Decide what happens to unexpected errors
Section titled “4. Decide what happens to unexpected errors”Anything else that escapes is logged with its stack trace and answered with a generic 500:
{"error":{"status":500,"message":"Internal server error","code":"internal_error"}}The exception’s text is never sent. To report crashes to an error tracker, set errorHandler. When it is set, the framework doesn’t log these errors itself:
final logger = ConsoleLogger(format: LogFormat.json);
final app = GisilaApp( config: AppConfig( logger: logger, errorHandler: (error, stack) { logger.error('Unhandled exception', error: error, stackTrace: stack); errorTracker.capture(error, stack); return jsonResponse(statusCode: 500, body: InternalServerError().toJson()); }, ),);The handler receives only errors that are not HttpExceptions, form validation errors or mapped database errors.
5. Return a Response for non-error special cases
Section titled “5. Return a Response for non-error special cases”Redirects, downloads and custom headers aren’t errors; return a Response:
@Get('/reports/{id}/download')Future<Response> download(String id, ReportService reports) async { final csv = await reports.render(id); return Response.ok(csv, headers: { 'content-type': 'text/csv; charset=utf-8', 'content-disposition': 'attachment; filename="report-$id.csv"', });}Verify it works
Section titled “Verify it works”test('withdraw beyond balance', () async { final res = await client.as(owner).post('/accounts/a1/withdraw', json: {'cents': 999999}); expect(res.statusCode, 422); expect(res.errorCode, 'insufficient_funds'); expect(res.error!['details'], {'balance': 1200});});
test('crashes are not leaked', () async { final res = await client.as(owner).get('/accounts/crash'); expect(res.statusCode, 500); expect(res.errorMessage, 'Internal server error');});Next steps
Section titled “Next steps”- Errors reference, including every framework error code
- Structured logging