From 19ab0d244d833aaaef6fbede04df5f520e60011d Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Thu, 16 Jul 2026 00:29:27 -0300 Subject: [PATCH 1/2] docs: Document sealed exception hierarchies --- .../03-error-handling-and-exceptions.md | 82 +++++++++++++++++++ docs/06-concepts/lookups/model-reference.md | 5 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md index c6a00f80..7dde78b2 100644 --- a/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md +++ b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md @@ -53,6 +53,88 @@ catch(e) { } ``` +### Group exceptions in a hierarchy + +Use `extends` to share fields and handling across related serializable exceptions. Add `sealed: true` to the root when all concrete cases are known and clients should handle them exhaustively. + +Define each exception in its own model file. For example, start with a sealed base exception: + +```yaml +# api_exception.spy.yaml +exception: ApiException +sealed: true +fields: + message: String +``` + +Then extend it with the concrete failures your endpoint can throw: + +```yaml +# not_found_exception.spy.yaml +exception: NotFoundException +extends: ApiException +fields: + resource: String +``` + +```yaml +# validation_exception.spy.yaml +exception: ValidationException +extends: ApiException +fields: + field: String +``` + +After running `serverpod generate`, throw the concrete exceptions from an endpoint: + +```dart +class UserEndpoint extends Endpoint { + Future updateName( + Session session, { + required int userId, + required String name, + }) async { + if (name.trim().isEmpty) { + throw ValidationException( + message: 'Enter a name.', + field: 'name', + ); + } + + var user = await User.db.findById(session, userId); + if (user == null) { + throw NotFoundException( + message: 'The user was not found.', + resource: 'user', + ); + } + + await User.db.updateRow(session, user.copyWith(name: name)); + } +} +``` + +On the client, catch the base type and use an exhaustive switch over its concrete subtypes: + +```dart +try { + await client.user.updateName(userId: userId, name: name); +} on ApiException catch (error) { + var message = switch (error) { + NotFoundException(:final resource) => '$resource was not found.', + ValidationException(:final field) => 'Check the $field field.', + }; + + showError(message); +} +``` + +The Dart analyzer requires every subtype of the sealed exception to be covered. This makes adding another concrete exception an explicit client change instead of silently falling through a general catch. + +Exception hierarchies can be defined in a [shared package](../data-and-the-database/models/shared-packages) when several Serverpod projects need the same wire types. Keep every subtype of a sealed shared exception in that same shared package. A consuming project cannot add another subtype to a sealed exception from a different package. + +An exception can only extend another exception, and a regular model class can only extend another regular model class. Serverpod reports a model validation error if a hierarchy mixes the two kinds. + ### Custom serializable exception classes If you already have a Dart exception class in a package shared by the server and client, the class must implement `SerializableException` for Serverpod to send it to the client. Otherwise, Serverpod treats the exception as an internal server error. diff --git a/docs/06-concepts/lookups/model-reference.md b/docs/06-concepts/lookups/model-reference.md index 8952268c..88b3288c 100644 --- a/docs/06-concepts/lookups/model-reference.md +++ b/docs/06-concepts/lookups/model-reference.md @@ -37,10 +37,11 @@ Every keyword available in a Serverpod model file, and whether it applies to a ` | [**default**](../data-and-the-database/models#default-values) | Sets the default value for both the model and the database. This keyword cannot be used with **relation**. | ✅ | | | | [**defaultModel**](../data-and-the-database/models#default-values) | Sets the default value for the model side. This keyword cannot be used with **relation**. | ✅ | | | | [**defaultPersist**](../data-and-the-database/models#default-values) | Sets the default value for the database side. This keyword cannot be used with **relation** and **!persist**. | ✅ | | | -| [**extends**](../data-and-the-database/models/inheritance-and-polymorphism#extending-a-class) | Specifies a parent class to inherit from. | ✅ | ✅ | | -| [**sealed**](../data-and-the-database/models/inheritance-and-polymorphism#sealed-classes) | Boolean flag to create a sealed class hierarchy, enabling exhaustive type checking. | ✅ | | | +| [**extends**](../data-and-the-database/models/inheritance-and-polymorphism#extending-a-class) | Specifies a parent class or exception of the same kind to inherit from. | ✅ | ✅ | | +| [**sealed**](../data-and-the-database/models/inheritance-and-polymorphism#sealed-classes) | Boolean flag to create a sealed class or exception hierarchy, enabling exhaustive type checking. | ✅ | ✅ | | ## Related - [Working with models](../data-and-the-database/models): how each keyword works, with examples. +- [Serializable exceptions](../endpoints-and-apis/error-handling-and-exceptions#serializable-exceptions): define, inherit, throw, and catch application errors. - [Database tables](../data-and-the-database/database/tables): the database-specific keywords (`table`, `relation`, `persist`, and indexing). From 4cb9304d30ffda06f160f47ae1bac6fe09ac432f Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Tue, 21 Jul 2026 15:45:48 -0300 Subject: [PATCH 2/2] docs: Polish the exception hierarchy section Use a noun-shaped heading like the neighbouring subsections, label the model files with the code block title attribute instead of a comment, link the inheritance page for extends and sealed, and tighten the closing paragraphs. --- .../03-error-handling-and-exceptions.md | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md index 7dde78b2..d64e378d 100644 --- a/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md +++ b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md @@ -53,32 +53,29 @@ catch(e) { } ``` -### Group exceptions in a hierarchy +### Exception hierarchies -Use `extends` to share fields and handling across related serializable exceptions. Add `sealed: true` to the root when all concrete cases are known and clients should handle them exhaustively. +Related failures often share fields and the same handling in the app. Use [`extends`](../data-and-the-database/models/inheritance-and-polymorphism) to put them in a hierarchy, and mark the root `sealed` when you know every concrete case and want the app to handle all of them. -Define each exception in its own model file. For example, start with a sealed base exception: +Define each exception in its own model file, starting with the sealed base: -```yaml -# api_exception.spy.yaml +```yaml title="api_exception.spy.yaml" exception: ApiException sealed: true fields: message: String ``` -Then extend it with the concrete failures your endpoint can throw: +Then extend it with the concrete failures the endpoint can throw: -```yaml -# not_found_exception.spy.yaml +```yaml title="not_found_exception.spy.yaml" exception: NotFoundException extends: ApiException fields: resource: String ``` -```yaml -# validation_exception.spy.yaml +```yaml title="validation_exception.spy.yaml" exception: ValidationException extends: ApiException fields: @@ -129,11 +126,11 @@ try { } ``` -The Dart analyzer requires every subtype of the sealed exception to be covered. This makes adding another concrete exception an explicit client change instead of silently falling through a general catch. +Because the base is sealed, the analyzer requires the switch to cover every subtype. Adding another concrete exception then becomes a deliberate change in the app instead of a case that quietly falls through to a general catch. -Exception hierarchies can be defined in a [shared package](../data-and-the-database/models/shared-packages) when several Serverpod projects need the same wire types. Keep every subtype of a sealed shared exception in that same shared package. A consuming project cannot add another subtype to a sealed exception from a different package. +A hierarchy can live in a [shared package](../data-and-the-database/models/shared-packages) when several Serverpod projects need the same wire types. Every subtype of a sealed exception has to stay in the package that declares the base, so a consuming project cannot add a case of its own. -An exception can only extend another exception, and a regular model class can only extend another regular model class. Serverpod reports a model validation error if a hierarchy mixes the two kinds. +Exceptions and regular model classes cannot be mixed in the same hierarchy. An exception only extends another exception, and a class only extends another class. Anything else fails validation when you generate the code. ### Custom serializable exception classes