From ea428ca1178cfe9424468e2310ef134928fc2f23 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 00:29:30 -0700 Subject: [PATCH 01/18] Model property error handling --- .../model-property-error-handling.md | 881 ++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 packages/graphql/letter-to-santa/model-property-error-handling.md diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md new file mode 100644 index 00000000000..931ecc879dc --- /dev/null +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -0,0 +1,881 @@ +# Proposal: Expressing Model Property Error Handling + +This proposal suggests adding a two new decorator to the TypeSpec standard library: + +- `@throws` decorator: This decorator is used to specify that the use of a model property may result in specific errors being produced. +- `@handles` decorator: This decorator is used to specify that an operation or property will handle certain types of errors, preventing them from being propagated. + +It also proposes updating existing emitters to support these new decorators. + +
+ +## Goals + +1. Provide a way for TSP developers to document errors associated with a particular model property. +2. Provide spec emitters with information that can be used to update the set of operation errors based on the models in use, and the error handling of the operation. +3. Allow code emitters to generate code that expects and handles these errors appropriately. + +
+ +## Definitions + +### `@throws` decorator + +````typespec +/** + * Specify that the use of this property may result in specific errors being produced. + * + * @param errors The list of error models that may be produced when using this property. + * + * @example + * + * ```typespec + * model User { + * @throws(NotFoundError, PermissionDeniedError, InvalidURLError) + * profilePictureUrl: string; + * } + * ``` + */ +extern dec throws(target: ModelProperty, ...errors: Model[]); +```` + +The decorator can be applied to model properties. It specifies that any operation or context using the decorated property may produce the listed errors. This allows consumers to anticipate and handle these errors appropriately. + +The `errors` parameter is a list of models that represent the possible errors that can be thrown. Each model must be decorated with the [`@error` decorator][error-decorator]. + +
+ +### `@handles` decorator + +````typespec +/** + * Specify that this operation will handle certain types of errors. + * + * @param errors The list of error models that will be handled by this operation. + * + * @example + * + * ```typespec + * @handles(InvalidURLError) + * op getUser(id: string): User | NotFoundError; + * ``` + */ +extern dec handles(target: Operation, ...errors: Model[]); +```` + +The decorator can be applied to operations. It specifies that the operation will handle the listed errors if they are thrown by accessing a property decorated with the `@throws` decorator, preventing them from being propagated to the client. + +@route("/user/{id}") +@get +op getUser(@path id: string): User | GenericError; +``` + +In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. However, the `InvalidURLError` is still propagated to the operation's response type. + +#### Operation errors + `@throws` decorator + +The `@throws` decorator can also be used in conjunction with an operation's return type. +In the examples above, we are specifying that `getUser()` may return a `GenericError` in addition to the errors that may be produced by the `profilePictureUrl` property or any other property. + +If an error type is specified in both the operation's return type and the `@throws` decorator, there is no conflict — the operation will include the error (once) in the list of possible errors. + +
+ +#### Operation errors + `@handles` decorator + +It is possible, and valid, that an operation both `@handles` an error and also has a return type that includes that error. In this case, the operation _will_ include the error in the list of possible errors for the operation. + +```typespec +@route("/user/{id}") +@get +@handles(InvalidURLError) +op getUser(@path id: string): User | InvalidURLError | GenericError; +``` + +Semantically, this indicates that the operation will handle the `InvalidURLError` error when produced by a model property, but that the operation itself may also return that error, outside the context of a model property. + +This becomes important when considering error inheritance. + +#### Error inheritance + `@handles` decorator + +Error handling is often handled generically. +When an error is specified in the `@handles` decorator, and there are additional errors that `extend` from it, those errors will also be considered as handled by the operation. + +For example, if we were to specify that `getUser()` handles `GenericError`, it would also handle any errors that extend from `GenericError`, such as `NotFoundError` and `PermissionDeniedError`. + +```typespec +@error +model GenericError { + message: string; +} + +@error +model NotFoundError extends GenericError { + @statusCode _: 404; +} + +@error +model PermissionDeniedError extends GenericError { + @statusCode _: 403; +} + +@route("/user/{id}") +@get +@handles(GenericError) +op getUser(@path id: string): User | GenericError; +``` + +Now, any errors thrown by any of the model properties used in the operation will not be added to the operation's error output if they extend from `GenericError`. + +This inheritance does _not_ apply to the `@throws` decorator. If a property is decorated with `@throws(GenericError)`, it is not considered to be decorated with `@throws(NotFoundError)` or `@throws(PermissionDeniedError)`, even though those errors extend from `GenericError`. + +Conversely, if a property is decorated with `@throws(NotFoundError)`, it is not considered to be decorated with `@throws(GenericError)`. + +This is meant to align with exception semantics common among many languages, where a specific exception type must be specified when thrown but a class or category of exceptions can be caught. + +
+ +## Implementations and Use Cases + +### HTTP/REST/OpenAPI + +In a typical HTTP/REST API where operations are represented by endpoints, the `@throws` decorator can provide more accurate return types for operations that contain properties that may fail. + +In a larger API, it may be quite difficult to track all of the errors that can occur within an operation when the errors can be generated by many different layers of an API stack. The `@throws` decorator helps give the developer a more complete view of the errors that an operation can produce. + +Let's say we have this definition of models: + +
Click to collapse + +```typespec +import "@typespec/http"; +using Http; + +@error +model GenericError { + message: string; +} + +@error +model NotFoundError extends GenericError { + @statusCode _: 404; +} + +@error +model PermissionDeniedError extends GenericError { + @statusCode _: 403; +} + +@error +model InvalidURLError extends GenericError { + @statusCode _: 500; +} + +model User { + @key id: string; + profilePictureUrl: string; +} +``` + +
+ +Now we define an operation that uses the `User` model: + +```typespec +@route("/user/{id}") +@get +op getUser(@path id: string): User | GenericError; +``` + +This will produce the following OpenAPI: + +
Click to collapse + + +```yaml +paths: + /user/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: "#/components/schemas/GenericError" +``` + +
+ +
+ +#### Using `@throws` decorator + +With the `@throws` decorator, we can specify that the `profilePictureUrl` property may produce errors when accessed: + +```typespec +model User { + @key id: string; + + @throws(NotFoundError, PermissionDeniedError, InvalidURLError) + profilePictureUrl: string; +} +``` + +Since the `User` model is used in the `getUser()` operation, the generated OpenAPI will now include the possible errors that can occur when accessing the `profilePictureUrl` property: + +
Click to collapse + +```yaml +paths: + /user/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "403": + description: Access is forbidden. + content: + application/json: + schema: + $ref: "#/components/schemas/PermissionDeniedError" + "404": + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundError" + "500": + description: Server error + content: + application/json: + schema: + $ref: "#/components/schemas/InvalidURLError" + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: "#/components/schemas/GenericError" +``` + +
+ +The definition of `getUser()` has not changed, but it is now emitted as if the return type was + +```typespec +User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; +``` + +
+ +#### Using `@handles` decorator + +Perhaps our `getUser()` operation is designed to handle the `InvalidURLError` error, while other operations may not do so. We can use the `@handles` decorator to specify that this operation will handle that error: + +```typespec +@route("/user/{id}") +@get +@handles(InvalidURLError) +op getUser(@path id: string): User | GenericError; +``` + +Now, despite the presence of a `User.profilePictureUrl` property that may produce an `InvalidURLError`, the OpenAPI will not include it in the list of possible errors for the `getUser()` operation: + +
Click to collapse + +```yaml +paths: + /user/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "403": + description: Access is forbidden. + content: + application/json: + schema: + $ref: "#/components/schemas/PermissionDeniedError" + "404": + description: The server cannot find the requested resource. + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundError" + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: "#/components/schemas/GenericError" +``` + +
+ +This is not limited to the `profilePictureUrl` property. Any property that is decorated with `@throws(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation. + +
+ +### GraphQL + +In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]. The `@throws` decorator can be used to document which errors may occur when resolving a specific field. For example, a field decorated with `@throws` could generate GraphQL schema documentation indicating the possible errors. + +Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], where errors are included in the possible value of a field using union types. In this case, the `@throws` decorator can be used to specify which errors must be included in that union type. + +The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. These decorators can be used to customize how errors in a `@throws` decorator are emitted in the GraphQL schema. + +For example, a `@propagate` decorator could be used to indicate that an error type, if produced, should be propagated to parent fields. In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. + +A `@asData` decorator could be used to indicate that an error type should be included in the ["errors as data" pattern][errors-as-data]. This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. + +The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. Specifying an error in the `@handles` decorator will: + +- omit the error from the union response type, if the error has the `@asData` decorator. +- prevent the error from triggering non-nullability of the field type, if the error has the `@propagate` decorator. The field may still be marked non-null through other errors or other means. + +#### Example + +This example shows all of the above in action: + +
Click to collapse + +```typespec +import "@typespec/graphql"; +using GraphQL; + +@error +@GraphQL.interface +model ServerError { + message: string; +} + +@error +@GraphQL.asData +@GraphQL.interface +model ClientError { + message: string; +} + +@error +@GraphQL.asData +@doc("The resource is not found.") +model NotFoundError extends ClientError { + message: string = "Not found"; +} + +@error +@doc("The user does not have permission to access the resource.") +model PermissionDeniedError extends ClientError { + message: string = "Permission denied"; +} + +enum Service { + SERVICE_A, + SERVICE_B, +} + +@error +@GraphQL.propagate +@doc("A timeout occurred while waiting for a response from an upstream service.") +model UpstreamTimeoutError extends ServerError { + service: Service; // the service that timed out +} + +@error +@GraphQL.propagate +@doc("A race condition occurred.") +model RaceConditionError extends ServerError {} + +@doc("Mark this entry as seen") +op markAsSeen(seen: boolean): boolean | RaceConditionError; + +@GraphQL.operationFields(markAsSeen) +model ActivityEntry { + @throws(PermissionDeniedError) ipAddress?: string; +} + +// In GraphQL, fields can take arguments. +// These are specified like operations in TypeSpec. +@doc("Users following this user") +@handles(RaceConditionError) op followers(type?: string): User[]; + +@GraphQL.operationFields(followers) +model User { + @throws(NotFoundError, PermissionDeniedError) profilePictureUrl: string; + + @doc("A log of the user's activity") + @throws(UpstreamTimeoutError) activity: ActivityEntry[]; +} +``` + +
+ +This could result in the following GraphQL: + +
Click to collapse + +```graphql +interface ClientError { + message: String +} + +interface ServerError { + message: String +} + +type NotFoundError implements ClientError { + """ + The resource is not found. + * This error appears in union responses. + """ + message: String +} + +type PermissionDeniedError implements ClientError { + """ + The user does not have permission to access the resource. + """ + message: String +} + +enum Service { + SERVICE_A + SERVICE_B +} + +type UpstreamTimeoutError implements ServerError { + """ + A timeout occurred while waiting for a response from an upstream service. + * This error is propagated to the parent field. + """ + message: String + service: Service +} + +union UserProfilePictureUrlResponse = + | String + | NotFoundError # NotFoundError is `@asData`, so it's added to the union + | ClientError # PermissionDeniedError does not use `@asData`, but it extends from ClientError which does +type User { + """ + A log of the user's activity + * this field is non-null because it `@throws(UpstreamTimeoutError)` (which propagates) + """ + activity: [ActivityEntry!]! + + """ + Users following this user + * this field is nullable because even though User.activity[].markAsSeen will propagate a RaceConditionError, followers `@handles(RaceConditionError)` + """ + followers(type: String): [User!] + + profilePictureUrl: UserProfilePictureUrlResponse +} + +union ActivityEntryIpAddressResponse = String | ClientError # PermissionDeniedError does not use `@asData`, but it extends from ClientError which does +type ActivityEntry { + ipAddress: ActivityEntryIpAddressResponse + + """ + Mark this entry as seen + * this field is non-null because it has RaceConditionError (which propagates) in its return type + """ + markAsSeen(seen: Boolean!): Boolean! +} +``` + +
+ +
+ +### Client libraries + +Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@throws` or operations decorated with `@handles`. + +#### Example: Kotlin + +In Kotlin, the `Result` type or sealed classes can be used. For example: + +
Click to collapse + +```kotlin +sealed class Error { + object NotFound : Error() + object PermissionDenied : Error() + object InvalidUrl : Error() +} + +data class User( + val id: String, + val profilePictureUrl: Result // Field with @throws decorator +) + +fun getUser(id: String): Result { + // Operation with @handles decorator + if (id.isEmpty()) { + return Result.failure(Error.NotFound) + } + return Result.success( + User( + id = id, + profilePictureUrl = Result.failure(Error.PermissionDenied) + ) + ) +} +``` + +
+ +This approach ensures that clients handle errors in a type-safe and idiomatic way. + +#### Example: Swift + +In Swift, the `Result` type can be used to represent fields or operations that may fail. For example: + +
Click to collapse + +```swift +enum Error: Swift.Error { + case notFound + case permissionDenied + case invalidUrl +} + +struct User { + let id: String + let profilePictureUrl: Result // Field with @throws decorator +} + +func getUser(id: String) -> Result { + // Operation with @handles decorator + if id.isEmpty { + return .failure(.notFound) + } + return .success( + User( + id: id, + profilePictureUrl: .failure(.permissionDenied) + ) + ) +} +``` + +
+ +This approach ensures that clients handle errors in a type-safe and idiomatic way. + +
+ +### Server libraries + +Server libraries should generate code that includes appropriate error handling stubs. For example, in languages with an error monad or result monad, these constructs should be used to represent fields or operations that may produce errors. This allows server implementations to handle errors explicitly and propagate them as needed. + +#### Example: Scala + +In Scala, the `Either` type can be used to handle errors for fields and operations: + +
Click to collapse + +```scala +sealed trait Error +case object NotFound extends Error +case object PermissionDenied extends Error +case object InvalidUrl extends Error + +case class User(id: String, profilePictureUrl: Either[Error, String]) // Field with @throws decorator + +def getUser(id: String): Either[Error, User] = { + // Operation with @handles decorator + if (id.isEmpty) { + Left(NotFound) + } else { + Right(User(id, Left(PermissionDenied))) + } +} +``` + +
+ +This approach ensures that server-side logic is clear and errors are propagated or handled as needed. + +#### Example: Rust + +In Rust, the `Result` type can be used to handle errors for fields and operations: + +
Click to collapse + +```rust +fn resolve_profile_picture_url(user_id: &str) -> Result { + // Simulate a permission check + if user_id == "restricted" { + return Err(Error::PermissionDenied); + } + Ok("https://example.com/profile.jpg".to_string()) +} + +fn get_user_handler(id: &str) -> Result { + let user = User { + id: id.to_string(), + profile_picture_url: resolve_profile_picture_url(id), + }; + Ok(user) +} +``` + +
+ +Here, the server explicitly handles errors when resolving the `profile_picture_url` field. + +
+ +## Use in request input + +The `@throws` and `@handles` decorators apply equally to input as they do to output. Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output. + +
+ +### `@throws` for Input Validation Errors + +When applied to input properties, the `@throws` decorator specifies the errors that may occur during the validation or processing of client-provided data. For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: + +```typespec +model CreateUserRequest { + @throws(InvalidEmailError, MissingFieldError) + email: string; + + @throws(InvalidPasswordError) + password: string; +} +``` + +These errors are generated by the server in response to invalid or incomplete input provided by the client. This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. However, the propagation of errors from model properties to the operation's response type works the same way for input as it does for output. + +
+ +### `@handles` for Input-Level Error Handling + +The `@handles` decorator can be used to specify which input-related errors are handled by the operation itself, preventing them from being propagated to the client. For example, an operation to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: + +```typespec +@route("/user") +@post +@handles(InvalidEmailError) +op createUser(request: CreateUserRequest): User | GenericError; +``` + +This behavior mirrors how `@handles` is used for output errors, allowing developers to control which errors are exposed to the client and which are handled internally. + +
+ +### Error Propagation for Input + +Errors specified in `@throws` on input properties propagate to operations unless explicitly handled with `@handles`. For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error response types because they are specified in the `CreateUserRequest` model: + +```typespec +@route("/user") +@post +op createUser(request: CreateUserRequest): + | User + | InvalidEmailError + | MissingFieldError + | InvalidPasswordError + | GenericError; +``` + +This ensures consistency between input and output error modeling. By default, errors propagate from input properties to operations, but operations can override this behavior with `@handles`. + +
+ +### Protocol-Specific Behavior + +Input-related errors can also be tied to specific protocol behaviors. For example, errors can be associated with HTTP status codes or GraphQL-specific behaviors. The following example shows how to use the `@statusCode` decorator to specify that `InvalidEmailError` and `MissingFieldError` should result in HTTP 400 responses: + +```typespec +@error +model InvalidEmailError { + @statusCode _: 400; + message: string; +} + +@error +model MissingFieldError { + @statusCode _: 400; + message: string; +} +``` + +This is consistent with how protocol-specific metadata is applied to output errors, ensuring that input errors are handled appropriately in the context of the protocol being used. + +
+ +## Alternatives Considered + +### Mimic error handling in operations + +TypeSpec [operations][operations] allow for specifying possible errors that the operation may produce via the [operation's return type][operations-return-type]. The standard pattern is to use a union type that includes the models representing the errors, which have been decorated with the [`@error` decorator][error-decorator]. + +For example: + +```typespec +@error +model NotFoundError { + message: string; +} + +op getUser(id: string): User | NotFoundError; +``` + +The `@throws` decorator is different from the return type of operations in that it is used to document errors that may occur when accessing a property. + +This distinction is useful when a property itself may inherently produce errors, regardless of the operation in which it is used. For example, accessing a property that requires a network fetch or a permission check may result in errors. + +Using a union type, as operations do, does not allow for the same error semantic in model properties. Instead, such a type would be indicative of possible types for the property's _value_: + +```typespec +model User { + profilePictureUrl: string | NotFoundError | PermissionDeniedError; +} +``` + +The above TypeSpec implies that the property could be populated with either a string or one of two model type. The fact that `NotFoundError` and `PermissionDeniedError` use the `@error` decorator is irrelevant. + +```typespec +model User { + @throws(NotFoundError, PermissionDeniedError) + profilePictureUrl: string; +} +``` + +This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. Typically, this means that the property does not _have_ a value in that scenario, and instead should be used to describe the appropriate error-returning semantic of a given protocol. + +## Summary + +The `@throws` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. While the mechanics of these decorators are identical for input and output, the use cases differ slightly. Input errors are generated by the server in response to invalid or incomplete client-provided data, whereas output errors are typically generated by the server's internal logic or data access operations. This distinction ensures that the proposal remains flexible and applicable to a wide range of error-handling scenarios. + +
+ +## Bonus: Adding Context Modifiers to `@error` + +As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. This does not change the core mechanics of the `@throws` and `@handles` decorators. + +### Proposed Definition + +The `@error` decorator would accept an optional argument specifying one or more visibility enums. + +````typespec +/** + * Specify that this model is an error type. Operations return error types when the operation has failed. + * + * @param contexts The list of contexts in which this error applies. This can be used to indicate whether the error is relevant for input, output, or both. + * + * @example + * ```typespec + * @error(Lifecycle.CREATE, Lifecycle.UPDATE) + * model PetStoreError { + * code: string; + * message: string; + * } + * ``` + */ +extern dec error(target: Model, ...contexts: valueof EnumMember[]); +```` + +For example: + +```typespec +@error(Lifecycle.CREATE, Lifecycle.UPDATE) +model InvalidEmailError { + message: string; +} + +@error(Lifecycle.READ) +model PermissionDeniedError { + message: string; +} +``` + +Here, `Lifecycle.CREATE` and `Lifecycle.UPDATE` indicate that `InvalidEmailError` applies in input contexts (e.g., when creating or updating a resource), while `Lifecycle.READ` indicates that `PermissionDeniedError` applies in output contexts (e.g., when reading a resource). + +Libraries and emitters should interpret context modifiers, when applied to error models, to determine what errors should be included in different contexts. This mirrors the [visibility system][visibility-system], and libraries and emitters should interpret the context modifiers the same way as they already do for visibility. + +### Examples + +The following examples illustrate how the context modifiers can be used in practice. + +#### Input Contexts + +Errors with `Lifecycle.CREATE`, `Lifecycle.UPDATE`, or `Lifecycle.DELETE` are included when the model is used in an input context. + +```typespec +@route("/user") +@post +op createUser(request: User): boolean | InvalidEmailError | GenericError; +``` + +#### Output Contexts + +Errors with `Lifecycle.READ` are included when the model is used in an output context. + +```typespec +@route("/user/{id}") +@get +op getUser(@path id: string): User | PermissionDeniedError | GenericError; +``` + +#### Both Contexts + +Errors can apply to both input and output contexts by specifying multiple lifecycle stages. + +```typespec +@error(Lifecycle.CREATE, Lifecycle.READ) +model GenericError { + message: string; +} +``` + +# + +[error-decorator]: https://typespec.io/docs/standard-library/built-in-decorators/#@error +[operations]: https://typespec.io/docs/language-basics/operations/ +[operations-return-type]: https://typespec.io/docs/language-basics/operations/#return-type +[graphql-errors]: https://graphql.org/learn/response/#errors +[errors-as-data]: https://www.apollographql.com/docs/graphos/schema-design/guides/errors-as-data-explained +[graphql-emitter]: https://github.com/microsoft/typespec/issues/4933 +[statuscode-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.statusCode +[visibility-system]: https://typespec.io/docs/language-basics/visibility/ From be62c60ee19384ca1e53e42068966869e3d25121 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 11:27:41 -0700 Subject: [PATCH 02/18] model properties can also `@handle` --- .../model-property-error-handling.md | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 931ecc879dc..d8d06e031d6 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -49,21 +49,36 @@ The `errors` parameter is a list of models that represent the possible errors th ````typespec /** - * Specify that this operation will handle certain types of errors. + * Specify that this operation or model property will handle certain types of errors. * - * @param errors The list of error models that will be handled by this operation. + * @param errors The list of error models that will be handled by this operation or model property. * * @example * * ```typespec - * @handles(InvalidURLError) - * op getUser(id: string): User | NotFoundError; + * @handles(InvalidURLError) op getUser(id: string): User | NotFoundError; + * + * model User { + * @handles(PermissionDeniedError) profilePictureUrl: string; + * } * ``` */ -extern dec handles(target: Operation, ...errors: Model[]); +extern dec handles(target: Operation | ModelProperty, ...errors: Model[]); ```` -The decorator can be applied to operations. It specifies that the operation will handle the listed errors if they are thrown by accessing a property decorated with the `@throws` decorator, preventing them from being propagated to the client. +The decorator can be applied to operations or model properties. It specifies that the operation or model property will handle the listed errors,preventing them from being propagated to the client. + +The `errors` parameter is a list of models that represent the errors that will be handled by the operation or model property. +Each model must be decorated with the [`@error` decorator][error-decorator]. + +For example, if a property handles an error internally, that error will not propagate to the operation's response type: + +```typespec +model User { + @throws(InvalidURLError) + @handles(PermissionDeniedError) + profilePictureUrl: string; +} @route("/user/{id}") @get @@ -96,6 +111,18 @@ Semantically, this indicates that the operation will handle the `InvalidURLError This becomes important when considering error inheritance. +#### `@throws` + `@handles` decorator + +Similarly, model properties may have one or more error types defined in both their `@throws` decorator and the `@handles` decorator. In this case, the error is still considered to be throwable by the model property. + +```typespec +model User { + @throws(InvalidURLError) + @handles(PermissionDeniedError, InvalidURLError) + profilePictureUrl: string; +} +``` + #### Error inheritance + `@handles` decorator Error handling is often handled generically. From dfe8f3e5aebf8e5f17e442fe990c00c0fd0b606e Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 11:33:46 -0700 Subject: [PATCH 03/18] formatting fixes --- .../model-property-error-handling.md | 119 ++++++++++++------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index d8d06e031d6..2421a12e485 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -39,9 +39,12 @@ It also proposes updating existing emitters to support these new decorators. extern dec throws(target: ModelProperty, ...errors: Model[]); ```` -The decorator can be applied to model properties. It specifies that any operation or context using the decorated property may produce the listed errors. This allows consumers to anticipate and handle these errors appropriately. +The decorator can be applied to model properties. +It specifies that any operation or context using the decorated property may produce the listed errors. +This allows consumers to anticipate and handle these errors appropriately. -The `errors` parameter is a list of models that represent the possible errors that can be thrown. Each model must be decorated with the [`@error` decorator][error-decorator]. +The `errors` parameter is a list of models that represent the possible errors that can be thrown. +Each model must be decorated with the [`@error` decorator][error-decorator].
@@ -66,7 +69,8 @@ The `errors` parameter is a list of models that represent the possible errors th extern dec handles(target: Operation | ModelProperty, ...errors: Model[]); ```` -The decorator can be applied to operations or model properties. It specifies that the operation or model property will handle the listed errors,preventing them from being propagated to the client. +The decorator can be applied to operations or model properties. +It specifies that the operation or model property will handle the listed errors,preventing them from being propagated to the client. The `errors` parameter is a list of models that represent the errors that will be handled by the operation or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. @@ -85,7 +89,8 @@ model User { op getUser(@path id: string): User | GenericError; ``` -In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. However, the `InvalidURLError` is still propagated to the operation's response type. +In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. +However, the `InvalidURLError` is still propagated to the operation's response type. #### Operation errors + `@throws` decorator @@ -98,7 +103,8 @@ If an error type is specified in both the operation's return type and the `@thro #### Operation errors + `@handles` decorator -It is possible, and valid, that an operation both `@handles` an error and also has a return type that includes that error. In this case, the operation _will_ include the error in the list of possible errors for the operation. +It is possible, and valid, that an operation both `@handles` an error and also has a return type that includes that error. +In this case, the operation _will_ include the error in the list of possible errors for the operation. ```typespec @route("/user/{id}") @@ -113,7 +119,8 @@ This becomes important when considering error inheritance. #### `@throws` + `@handles` decorator -Similarly, model properties may have one or more error types defined in both their `@throws` decorator and the `@handles` decorator. In this case, the error is still considered to be throwable by the model property. +Similarly, model properties may have one or more error types defined in both their `@throws` decorator and the `@handles` decorator. +In this case, the error is still considered to be throwable by the model property. ```typespec model User { @@ -154,7 +161,8 @@ op getUser(@path id: string): User | GenericError; Now, any errors thrown by any of the model properties used in the operation will not be added to the operation's error output if they extend from `GenericError`. -This inheritance does _not_ apply to the `@throws` decorator. If a property is decorated with `@throws(GenericError)`, it is not considered to be decorated with `@throws(NotFoundError)` or `@throws(PermissionDeniedError)`, even though those errors extend from `GenericError`. +This inheritance does _not_ apply to the `@throws` decorator. +If a property is decorated with `@throws(GenericError)`, it is not considered to be decorated with `@throws(NotFoundError)` or `@throws(PermissionDeniedError)`, even though those errors extend from `GenericError`. Conversely, if a property is decorated with `@throws(NotFoundError)`, it is not considered to be decorated with `@throws(GenericError)`. @@ -168,7 +176,8 @@ This is meant to align with exception semantics common among many languages, whe In a typical HTTP/REST API where operations are represented by endpoints, the `@throws` decorator can provide more accurate return types for operations that contain properties that may fail. -In a larger API, it may be quite difficult to track all of the errors that can occur within an operation when the errors can be generated by many different layers of an API stack. The `@throws` decorator helps give the developer a more complete view of the errors that an operation can produce. +In a larger API, it may be quite difficult to track all of the errors that can occur within an operation when the errors can be generated by many different layers of an API stack. +The `@throws` decorator helps give the developer a more complete view of the errors that an operation can produce. Let's say we have this definition of models: @@ -217,7 +226,6 @@ op getUser(@path id: string): User | GenericError; This will produce the following OpenAPI:
Click to collapse - ```yaml paths: @@ -322,7 +330,8 @@ User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; #### Using `@handles` decorator -Perhaps our `getUser()` operation is designed to handle the `InvalidURLError` error, while other operations may not do so. We can use the `@handles` decorator to specify that this operation will handle that error: +Perhaps our `getUser()` operation is designed to handle the `InvalidURLError` error, while other operations may not do so. +We can use the `@handles` decorator to specify that this operation will handle that error: ```typespec @route("/user/{id}") @@ -375,26 +384,35 @@ paths:
-This is not limited to the `profilePictureUrl` property. Any property that is decorated with `@throws(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation. +This is not limited to the `profilePictureUrl` property. +Any property that is decorated with `@throws(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation.
### GraphQL -In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]. The `@throws` decorator can be used to document which errors may occur when resolving a specific field. For example, a field decorated with `@throws` could generate GraphQL schema documentation indicating the possible errors. +In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]. +The `@throws` decorator can be used to document which errors may occur when resolving a specific field. +For example, a field decorated with `@throws` could generate GraphQL schema documentation indicating the possible errors. -Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], where errors are included in the possible value of a field using union types. In this case, the `@throws` decorator can be used to specify which errors must be included in that union type. +Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], where errors are included in the possible value of a field using union types. +In this case, the `@throws` decorator can be used to specify which errors must be included in that union type. -The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. These decorators can be used to customize how errors in a `@throws` decorator are emitted in the GraphQL schema. +The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. +These decorators can be used to customize how errors in a `@throws` decorator are emitted in the GraphQL schema. -For example, a `@propagate` decorator could be used to indicate that an error type, if produced, should be propagated to parent fields. In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. +For example, a `@propagate` decorator could be used to indicate that an error type, if produced, should be propagated to parent fields. +In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. -A `@asData` decorator could be used to indicate that an error type should be included in the ["errors as data" pattern][errors-as-data]. This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. +A `@asData` decorator could be used to indicate that an error type should be included in the ["errors as data" pattern][errors-as-data]. +This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. -The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. Specifying an error in the `@handles` decorator will: +The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. +Specifying an error in the `@handles` decorator will: - omit the error from the union response type, if the error has the `@asData` decorator. -- prevent the error from triggering non-nullability of the field type, if the error has the `@propagate` decorator. The field may still be marked non-null through other errors or other means. +- prevent the error from triggering non-nullability of the field type, if the error has the `@propagate` decorator. + The field may still be marked non-null through other errors or other means. #### Example @@ -553,11 +571,13 @@ type ActivityEntry { ### Client libraries -Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@throws` or operations decorated with `@handles`. +Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. +For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@throws` or operations decorated with `@handles`. #### Example: Kotlin -In Kotlin, the `Result` type or sealed classes can be used. For example: +In Kotlin, the `Result` type or sealed classes can be used. +For example:
Click to collapse @@ -593,7 +613,8 @@ This approach ensures that clients handle errors in a type-safe and idiomatic wa #### Example: Swift -In Swift, the `Result` type can be used to represent fields or operations that may fail. For example: +In Swift, the `Result` type can be used to represent fields or operations that may fail. +For example:
Click to collapse @@ -631,7 +652,9 @@ This approach ensures that clients handle errors in a type-safe and idiomatic wa ### Server libraries -Server libraries should generate code that includes appropriate error handling stubs. For example, in languages with an error monad or result monad, these constructs should be used to represent fields or operations that may produce errors. This allows server implementations to handle errors explicitly and propagate them as needed. +Server libraries should generate code that includes appropriate error handling stubs. +For example, in languages with an error monad or result monad, these constructs should be used to represent fields or operations that may produce errors. +This allows server implementations to handle errors explicitly and propagate them as needed. #### Example: Scala @@ -693,13 +716,16 @@ Here, the server explicitly handles errors when resolving the `profile_picture_u ## Use in request input -The `@throws` and `@handles` decorators apply equally to input as they do to output. Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output. +The `@throws` and `@handles` decorators apply equally to input as they do to output. +Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. +The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output.
### `@throws` for Input Validation Errors -When applied to input properties, the `@throws` decorator specifies the errors that may occur during the validation or processing of client-provided data. For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: +When applied to input properties, the `@throws` decorator specifies the errors that may occur during the validation or processing of client-provided data. +For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: ```typespec model CreateUserRequest { @@ -711,13 +737,16 @@ model CreateUserRequest { } ``` -These errors are generated by the server in response to invalid or incomplete input provided by the client. This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. However, the propagation of errors from model properties to the operation's response type works the same way for input as it does for output. +These errors are generated by the server in response to invalid or incomplete input provided by the client. +This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. +However, the propagation of errors from model properties to the operation's response type works the same way for input as it does for output.
### `@handles` for Input-Level Error Handling -The `@handles` decorator can be used to specify which input-related errors are handled by the operation itself, preventing them from being propagated to the client. For example, an operation to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: +The `@handles` decorator can be used to specify which input-related errors are handled by the operation itself, preventing them from being propagated to the client. +For example, an operation to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: ```typespec @route("/user") @@ -732,7 +761,8 @@ This behavior mirrors how `@handles` is used for output errors, allowing develop ### Error Propagation for Input -Errors specified in `@throws` on input properties propagate to operations unless explicitly handled with `@handles`. For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error response types because they are specified in the `CreateUserRequest` model: +Errors specified in `@throws` on input properties propagate to operations unless explicitly handled with `@handles`. +For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error response types because they are specified in the `CreateUserRequest` model: ```typespec @route("/user") @@ -745,13 +775,16 @@ op createUser(request: CreateUserRequest): | GenericError; ``` -This ensures consistency between input and output error modeling. By default, errors propagate from input properties to operations, but operations can override this behavior with `@handles`. +This ensures consistency between input and output error modeling. +By default, errors propagate from input properties to operations, but operations can override this behavior with `@handles`.
### Protocol-Specific Behavior -Input-related errors can also be tied to specific protocol behaviors. For example, errors can be associated with HTTP status codes or GraphQL-specific behaviors. The following example shows how to use the `@statusCode` decorator to specify that `InvalidEmailError` and `MissingFieldError` should result in HTTP 400 responses: +Input-related errors can also be tied to specific protocol behaviors. +For example, errors can be associated with HTTP status codes or GraphQL-specific behaviors. +The following example shows how to use the `@statusCode` decorator to specify that `InvalidEmailError` and `MissingFieldError` should result in HTTP 400 responses: ```typespec @error @@ -775,7 +808,8 @@ This is consistent with how protocol-specific metadata is applied to output erro ### Mimic error handling in operations -TypeSpec [operations][operations] allow for specifying possible errors that the operation may produce via the [operation's return type][operations-return-type]. The standard pattern is to use a union type that includes the models representing the errors, which have been decorated with the [`@error` decorator][error-decorator]. +TypeSpec [operations][operations] allow for specifying possible errors that the operation may produce via the [operation's return type][operations-return-type]. +The standard pattern is to use a union type that includes the models representing the errors, which have been decorated with the [`@error` decorator][error-decorator]. For example: @@ -790,9 +824,11 @@ op getUser(id: string): User | NotFoundError; The `@throws` decorator is different from the return type of operations in that it is used to document errors that may occur when accessing a property. -This distinction is useful when a property itself may inherently produce errors, regardless of the operation in which it is used. For example, accessing a property that requires a network fetch or a permission check may result in errors. +This distinction is useful when a property itself may inherently produce errors, regardless of the operation in which it is used. +For example, accessing a property that requires a network fetch or a permission check may result in errors. -Using a union type, as operations do, does not allow for the same error semantic in model properties. Instead, such a type would be indicative of possible types for the property's _value_: +Using a union type, as operations do, does not allow for the same error semantic in model properties. +Instead, such a type would be indicative of possible types for the property's _value_: ```typespec model User { @@ -800,7 +836,8 @@ model User { } ``` -The above TypeSpec implies that the property could be populated with either a string or one of two model type. The fact that `NotFoundError` and `PermissionDeniedError` use the `@error` decorator is irrelevant. +The above TypeSpec implies that the property could be populated with either a string or one of two model type. +The fact that `NotFoundError` and `PermissionDeniedError` use the `@error` decorator is irrelevant. ```typespec model User { @@ -809,17 +846,24 @@ model User { } ``` -This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. Typically, this means that the property does not _have_ a value in that scenario, and instead should be used to describe the appropriate error-returning semantic of a given protocol. +This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. +Typically, this means that the property does not _have_ a value in that scenario, and instead should be used to describe the appropriate error-returning semantic of a given protocol. ## Summary -The `@throws` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. While the mechanics of these decorators are identical for input and output, the use cases differ slightly. Input errors are generated by the server in response to invalid or incomplete client-provided data, whereas output errors are typically generated by the server's internal logic or data access operations. This distinction ensures that the proposal remains flexible and applicable to a wide range of error-handling scenarios. +The `@throws` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. +While the mechanics of these decorators are identical for input and output, the use cases differ slightly. +Input errors are generated by the server in response to invalid or incomplete client-provided data, whereas output errors are typically generated by the server's internal logic or data access operations. +This distinction ensures that the proposal remains flexible and applicable to a wide range of error-handling scenarios.
## Bonus: Adding Context Modifiers to `@error` -As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. This does not change the core mechanics of the `@throws` and `@handles` decorators. +As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. +This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. +This enhancement would provide additional clarity and flexibility when modeling errors. +This does not change the core mechanics of the `@throws` and `@handles` decorators. ### Proposed Definition @@ -859,7 +903,8 @@ model PermissionDeniedError { Here, `Lifecycle.CREATE` and `Lifecycle.UPDATE` indicate that `InvalidEmailError` applies in input contexts (e.g., when creating or updating a resource), while `Lifecycle.READ` indicates that `PermissionDeniedError` applies in output contexts (e.g., when reading a resource). -Libraries and emitters should interpret context modifiers, when applied to error models, to determine what errors should be included in different contexts. This mirrors the [visibility system][visibility-system], and libraries and emitters should interpret the context modifiers the same way as they already do for visibility. +Libraries and emitters should interpret context modifiers, when applied to error models, to determine what errors should be included in different contexts. +This mirrors the [visibility system][visibility-system], and libraries and emitters should interpret the context modifiers the same way as they already do for visibility. ### Examples From 0ca993bd3e1e80db327fd8d2233c467b7e37f4c6 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 12:00:06 -0700 Subject: [PATCH 04/18] compiler warning --- .../model-property-error-handling.md | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 2421a12e485..eedff4622b9 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -1,6 +1,6 @@ # Proposal: Expressing Model Property Error Handling -This proposal suggests adding a two new decorator to the TypeSpec standard library: +This proposal suggests adding two new decorators to the TypeSpec standard library: - `@throws` decorator: This decorator is used to specify that the use of a model property may result in specific errors being produced. - `@handles` decorator: This decorator is used to specify that an operation or property will handle certain types of errors, preventing them from being propagated. @@ -858,7 +858,9 @@ This distinction ensures that the proposal remains flexible and applicable to a
-## Bonus: Adding Context Modifiers to `@error` +## Additional Considerations + +### Adding Context Modifiers to `@error` As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. @@ -906,11 +908,11 @@ Here, `Lifecycle.CREATE` and `Lifecycle.UPDATE` indicate that `InvalidEmailError Libraries and emitters should interpret context modifiers, when applied to error models, to determine what errors should be included in different contexts. This mirrors the [visibility system][visibility-system], and libraries and emitters should interpret the context modifiers the same way as they already do for visibility. -### Examples +#### Examples The following examples illustrate how the context modifiers can be used in practice. -#### Input Contexts +##### Input Contexts Errors with `Lifecycle.CREATE`, `Lifecycle.UPDATE`, or `Lifecycle.DELETE` are included when the model is used in an input context. @@ -920,7 +922,7 @@ Errors with `Lifecycle.CREATE`, `Lifecycle.UPDATE`, or `Lifecycle.DELETE` are in op createUser(request: User): boolean | InvalidEmailError | GenericError; ``` -#### Output Contexts +##### Output Contexts Errors with `Lifecycle.READ` are included when the model is used in an output context. @@ -930,7 +932,7 @@ Errors with `Lifecycle.READ` are included when the model is used in an output co op getUser(@path id: string): User | PermissionDeniedError | GenericError; ``` -#### Both Contexts +##### Both Contexts Errors can apply to both input and output contexts by specifying multiple lifecycle stages. @@ -941,6 +943,49 @@ model GenericError { } ``` +
+ +### Identifying Unused Error Handlers + +TypeSpec only knows, and can only reason about, errors that are specified in a `@throws` decorator. +If an error is specified in a `@handles` decorator but not in any `@throws` decorator of all the model properties that are part of that property or operation, the TypeSpec compiler will not be able to determine whether the error is actually used. + +To help developers make that determination, the TypeSpec compiler will issue a warning when this scenario occurs. +If the developer determines that the error _is_ thrown outside of the context of TypeSpec, they can use the standard [`# suppress` directive][suppress-directive] to suppress the warning. + +This warning helps to avoid misleading consumers about an error type that may not actually occur. + +#### Example: Unused Error Handler + +Consider the following example: + +```typespec +@error +model NotFoundError { + message: string; +} + +@error +model PermissionDeniedError { + message: string; +} + +model User { + @throws(NotFoundError) + profilePictureUrl: string; +} + +@route("/user/{id}") +@get +@handles(PermissionDeniedError) // warning +op getUser(@path id: string): User; +``` + +In this example, the `getUser` operation specifies that it handles `PermissionDeniedError` using the `@handles` decorator. +However, none of the properties or operations used in `getUser` (in this case, just the `User.profilePictureUrl` property) specify `PermissionDeniedError` in their `@throws` decorators. + +As a result, the TypeSpec compiler will issue a warning. + # [error-decorator]: https://typespec.io/docs/standard-library/built-in-decorators/#@error @@ -951,3 +996,4 @@ model GenericError { [graphql-emitter]: https://github.com/microsoft/typespec/issues/4933 [statuscode-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.statusCode [visibility-system]: https://typespec.io/docs/language-basics/visibility/ +[suppress-directive]: https://typespec.io/docs/language-basics/directives/#suppress From 36e0760e36438284df7c114d0ce614ee7b96a159 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 12:12:47 -0700 Subject: [PATCH 05/18] small fixes --- .../graphql/letter-to-santa/model-property-error-handling.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index eedff4622b9..1651763767d 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -70,7 +70,7 @@ extern dec handles(target: Operation | ModelProperty, ...errors: Model[]); ```` The decorator can be applied to operations or model properties. -It specifies that the operation or model property will handle the listed errors,preventing them from being propagated to the client. +It specifies that the operation or model property will handle the listed errors, preventing them from being propagated to the client. The `errors` parameter is a list of models that represent the errors that will be handled by the operation or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. @@ -847,7 +847,7 @@ model User { ``` This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. -Typically, this means that the property does not _have_ a value in that scenario, and instead should be used to describe the appropriate error-returning semantic of a given protocol. +Typically, this means that the property does not _have_ a value in that scenario and instead should be used to describe the appropriate error-returning semantic of a given protocol. ## Summary From f0472f4a3d9afbe883797fb9a89b38602ffe666c Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 4 Apr 2025 12:38:00 -0700 Subject: [PATCH 06/18] Add protobuf example --- .../model-property-error-handling.md | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 1651763767d..b73610fc5dd 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -172,6 +172,8 @@ This is meant to align with exception semantics common among many languages, whe ## Implementations and Use Cases +Below we list some proposed implementations in various emitter targets. These are meant to be illustrative of the effects of the `@throws` and `@handles` decorators, and are not proposing any of the specific syntax or implementation shown below. + ### HTTP/REST/OpenAPI In a typical HTTP/REST API where operations are represented by endpoints, the `@throws` decorator can provide more accurate return types for operations that contain properties that may fail. @@ -569,6 +571,143 @@ type ActivityEntry {
+### Protocol Buffers (Protobuf) + +Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. While Protobuf itself does not have a built-in concept of errors, the `@throws` and `@handles` decorators can be used to model and document errors in TypeSpec, which can then be translated into Protobuf-compatible patterns. Different patterns for communicating errors in Protobuf can be expressed using additional TypeSpec decorators. + +#### Using `@throws` with Protobuf + +The `@throws` decorator can be used to specify errors that may occur when accessing a property. These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. + +For example: + +
Click to collapse + +```typespec +@error +@oneOfError +model NotFoundError { + message: string; +} + +@error +@oneOfError +model PermissionDeniedError { + message: string; +} + +model User { + @throws(NotFoundError, PermissionDeniedError) + profilePictureUrl: string; +} + +@route("/user/{id}") +@get +op getUser(@path id: string): User; +``` + +
+ +This could be translated into the following Protobuf schema: + +
Click to collapse + +```proto +message NotFoundError { + string message = 1; +} + +message PermissionDeniedError { + string message = 1; +} + +message User { + string profilePictureUrl = 1; +} + +message GetUserResponse { + oneof result { + User user = 1; + NotFoundError not_found_error = 2; + PermissionDeniedError permission_denied_error = 3; + } +} + +service UserService { + rpc GetUser(GetUserRequest) returns (GetUserResponse); +} + +message GetUserRequest { + string id = 1; +} +``` + +
+ +#### Using gRPC Status Codes + +When using Protobuf with gRPC, errors are often communicated using gRPC's built-in status codes and error details. +These could be expressed in TypeSpec using a `@statusCode` decorator from a gRPC library, along with a generic `Error` model in the operation's return type. + +
Click to collapse + +```typespec +@error +model Error { + code: gRPC.StatusCode; + message: string; +} + +@error +model NotFoundError extends Error { + code: gRPC.StatusCode.NOT_FOUND; +} + +model User { + @throws(NotFoundError) profilePictureUrl: string; +} + +@route("/user/{id}") +@get +op getUser(@path id: string): User | Error; +``` + +
+ +This could be translated into the following Protobuf schema: + +
Click to collapse + +```proto +message Error { + GrpcStatusCode code = 1; + string message = 2; +} + +message User { + string profilePictureUrl = 1; +} + +message GetUserResponse { + oneof result { + User user = 1; + Error error = 2; + } +} + +service UserService { + rpc GetUser(GetUserRequest) returns (GetUserResponse); +} + +message GetUserRequest { + string id = 1; +} +``` + +
+ +
+ ### Client libraries Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. From 8f1448f8435c754f7fa2bc4ffcf3561fb3934544 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Tue, 8 Apr 2025 23:14:05 -0700 Subject: [PATCH 07/18] Add Thrift --- .../model-property-error-handling.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index b73610fc5dd..8cf8d92d986 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -708,6 +708,93 @@ message GetUserRequest {
+### Apache Thrift + +Apache Thrift supports defining exceptions as part of its IDL (Interface Definition Language), which makes it well-suited for modeling error using the `@throws` and `@handles` decorators. + +#### Using `@throws` with Thrift + +Exceptions specified by `@throws` can be represented in Thrift by defining exception types and including them in the `throws` clause of a service method. + +For example: + +
Click to collapse + +```typespec +@error +model NotFoundError { + message: string; +} + +@error +model PermissionDeniedError { + message: string; +} + +model User { + @throws(NotFoundError, PermissionDeniedError) + profilePictureUrl: string; +} + +@route("/user/{id}") +@get +op getUser(@path id: string): User; +``` + +
+ +This could be translated into the following Thrift IDL: + +
Click to collapse + +```thrift +exception NotFoundError { + 1: string message; +} + +exception PermissionDeniedError { + 1: string message; +} + +struct User { + 1: string profilePictureUrl; +} + +service UserService { + User getUser(1: string id) throws ( + 1: NotFoundError notFoundError, + 2: PermissionDeniedError permissionDeniedError + ); +} +``` + +
+ +#### Using `@handles` with Thrift + +The `@handles` decorator can be used to specify which exceptions are handled internally by an operation or property. In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. + +For example: + +```typespec +@route("/user/{id}") +@get +@handles(PermissionDeniedError) +op getUser(@path id: string): User; +``` + +If `PermissionDeniedError` is handled internally, the Thrift IDL would look like this: + +```thrift +service UserService { + User getUser(1: string id) throws ( + 1: NotFoundError notFoundError + ); +} +``` + +
+ ### Client libraries Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. From ae18332a3394adc56b7daa4a03d88571a5df2a70 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Wed, 9 Apr 2025 01:17:03 -0700 Subject: [PATCH 08/18] Further explain the relationship between error contexts and visibility --- .../model-property-error-handling.md | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 8cf8d92d986..3f9d7427388 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -1086,12 +1086,13 @@ This distinction ensures that the proposal remains flexible and applicable to a ## Additional Considerations -### Adding Context Modifiers to `@error` +### Optional: Adding Context Modifiers to `@error` As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. -This does not change the core mechanics of the `@throws` and `@handles` decorators. + +**This does not change the core mechanics of the `@throws` and `@handles` decorators, and the proposal for those decorators remains unchanged whether or not this enhancement is adopted.** ### Proposed Definition @@ -1105,7 +1106,7 @@ The `@error` decorator would accept an optional argument specifying one or more * * @example * ```typespec - * @error(Lifecycle.CREATE, Lifecycle.UPDATE) + * @error(Lifecycle.Create, Lifecycle.Update) * model PetStoreError { * code: string; * message: string; @@ -1118,18 +1119,18 @@ extern dec error(target: Model, ...contexts: valueof EnumMember[]); For example: ```typespec -@error(Lifecycle.CREATE, Lifecycle.UPDATE) +@error(Lifecycle.Create, Lifecycle.Update) model InvalidEmailError { message: string; } -@error(Lifecycle.READ) +@error(Lifecycle.Read) model PermissionDeniedError { message: string; } ``` -Here, `Lifecycle.CREATE` and `Lifecycle.UPDATE` indicate that `InvalidEmailError` applies in input contexts (e.g., when creating or updating a resource), while `Lifecycle.READ` indicates that `PermissionDeniedError` applies in output contexts (e.g., when reading a resource). +Here, `Lifecycle.Create` and `Lifecycle.Update` indicate that `InvalidEmailError` applies in input contexts (e.g., when creating or updating a resource), while `Lifecycle.Read` indicates that `PermissionDeniedError` applies in output contexts (e.g., when reading a resource). Libraries and emitters should interpret context modifiers, when applied to error models, to determine what errors should be included in different contexts. This mirrors the [visibility system][visibility-system], and libraries and emitters should interpret the context modifiers the same way as they already do for visibility. @@ -1140,17 +1141,36 @@ The following examples illustrate how the context modifiers can be used in pract ##### Input Contexts -Errors with `Lifecycle.CREATE`, `Lifecycle.UPDATE`, or `Lifecycle.DELETE` are included when the model is used in an input context. +By default, errors with `Lifecycle.Create`, `Lifecycle.Update`, or `Lifecycle.Delete` are included when the model is used as a parameter in the respective context. + +
Click to collapse ```typespec -@route("/user") -@post -op createUser(request: User): boolean | InvalidEmailError | GenericError; +@error(Lifecycle.CREATE, Lifecycle.UPDATE) +model InvalidEmailError { + message: string; +} + +model User { + @key id: string; + + @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) + @throws(InvalidEmailError) + email: string; +} + +@get op getUser(id: string): User | UserNotFound; // will not include InvalidEmailError, does return email field + +@post op createUser(...User): User; // will include InvalidEmailError, does return email field + +@delete op deleteUser(id: string): User; // does not include InvalidEmailError, does not return email field ``` +
+ ##### Output Contexts -Errors with `Lifecycle.READ` are included when the model is used in an output context. +By default, errors with `Lifecycle.Read` are included when the model is used in an output context. ```typespec @route("/user/{id}") @@ -1163,12 +1183,22 @@ op getUser(@path id: string): User | PermissionDeniedError | GenericError; Errors can apply to both input and output contexts by specifying multiple lifecycle stages. ```typespec -@error(Lifecycle.CREATE, Lifecycle.READ) +@error(Lifecycle.Create, Lifecycle.Read) model GenericError { message: string; } ``` +#### Context follows visibility + +There are a number of ways to modify the visibility of a model or operation. Context modifiers, as applied to errors, will follow the same rules as they do for visibility. + +For example, use of the [`@parameterVisibility`][parameter-visibility] or [`@returnTypeVisibility`][return-type-visibility] decorators will modify the visibility of the error model in the same way as it does for parameters. That is, the properties of a model used as a parameter will apply their `@throws` errors based on the visibility of parameters. The properties of a model used as a return type will apply their `@throws` errors based on the visibility of the return type. + +This also means that decorators which apply implicit visibility, such as [`@post`][post-decorator] or [`@put`][put-decorator], will apply the implicit visibility of the operation to the error model. + +Any other modification of visibility including visibility filters, custom context classes, et. al. should affect errors in the same way as they affect model properties. +
### Identifying Unused Error Handlers @@ -1223,3 +1253,7 @@ As a result, the TypeSpec compiler will issue a warning. [statuscode-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.statusCode [visibility-system]: https://typespec.io/docs/language-basics/visibility/ [suppress-directive]: https://typespec.io/docs/language-basics/directives/#suppress +[parameter-visibility]: https://typespec.io/docs/standard-library/built-in-decorators/#@parameterVisibility +[return-type-visibility]: https://typespec.io/docs/standard-library/built-in-decorators/#@returnTypeVisibility +[post-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.post +[put-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.put From 478cfcde54cbb9011f3b07f6b9d39032b54bfad0 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Wed, 9 Apr 2025 01:28:06 -0700 Subject: [PATCH 09/18] Add discussion on rejecting the alternative of `@throws` and `@handles` decorators --- .../model-property-error-handling.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 3f9d7427388..b5636f575e0 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -1189,6 +1189,10 @@ model GenericError { } ``` +##### No contexts + +Just as is true for visibility, if no context is specified, the error model [will be included in all of the default context modifiers][default-visibility] applied by default by the visibility class. + #### Context follows visibility There are a number of ways to modify the visibility of a model or operation. Context modifiers, as applied to errors, will follow the same rules as they do for visibility. @@ -1199,6 +1203,34 @@ This also means that decorators which apply implicit visibility, such as [`@post Any other modification of visibility including visibility filters, custom context classes, et. al. should affect errors in the same way as they affect model properties. +#### Rejected alternative: Context modifiers on `@throws` and `@handles` + +An alternative to adding context modifiers to the `@error` decorator is to add them to the `@throws` and `@handles` decorators. + +This would allow developers to specify the context in which an error applies model property by model property, rather than applying to an error model everywhere it appears. + +Such an alternative approach might look something like: + +```typespec +model User { + @key id: string; + + @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) + @throws([InvalidEmailError], [Lifecycle.Create, Lifecycle.Update]) + email: string; +} +``` + +While this approach does allow for finer granularity in specifying the context in which an error applies, it also adds complexity to the `@throws` and `@handles` decorators — and complexity for the developer to reason about the context in which an error applies. +Applying context modifiers to the `@error` decorator abstracts the concerns of context away from any particular field or operation, so the developer does not always need to be considering it. +It seems fairly intuitive for a developer to specify that an `InvalidParametersError` would only apply in input contexts, while a `PermissionDeniedError` would only apply in output contexts. + +If context modifiers are specified on the `@throws` and `@handles` decorators, it is likely that the developer forgets to add all of the relevant lifecycle modifiers in some cases. +This would result in operations insufficiently specifying errors, leading to clients receiving errors that they do not expect from the spec. + +By contrast, adding context modifiers to the `@error` decorator is more likely to add errors in more contexts than are needed; while not ideal, specifying extra errors in the spec that will never be returned is less problematic than omitting errors that will be. +Indeed, there's no guarantee that _any_ error specified ever actually will be. +
### Identifying Unused Error Handlers @@ -1257,3 +1289,4 @@ As a result, the TypeSpec compiler will issue a warning. [return-type-visibility]: https://typespec.io/docs/standard-library/built-in-decorators/#@returnTypeVisibility [post-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.post [put-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.put +[default-visibility]: https://typespec.io/docs/language-basics/visibility/#basic-concepts From 8f4108b3b79ae435f2c2b1604a267e852bc54156 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Wed, 9 Apr 2025 01:45:44 -0700 Subject: [PATCH 10/18] Discuss return type error vs. `@throws` error --- .../letter-to-santa/model-property-error-handling.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index b5636f575e0..fe910ae9654 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -99,6 +99,15 @@ In the examples above, we are specifying that `getUser()` may return a `GenericE If an error type is specified in both the operation's return type and the `@throws` decorator, there is no conflict — the operation will include the error (once) in the list of possible errors. +Semantically, the distinction between a `@throws` decorator and the operation's return type is in where the error is communicated. +An error on a return type indicates that the error is somehow exposed directly in that return type. +An error specified with `@throws`, on the other hand, may appear in a different location depending on if or where the error is is specified in a `@handles` decorator. + +For instance, a bulk operation of some kind that includes the results of several sub-operations could communicate errors in a few different ways. +One way would be for each of the operations in the bulk set to provide its error value as its specific return type — as indicated by an error present in the return type. +Another might be for the bulk operation to aggregate all of the errors that occurred in the sub-operations and communicate them somewhere in its own response, which could be accomplished by the sub-operations using the `@throws` decorator and the bulk operation using the `@handles` decorator. +Essentially, an error in a return type is opted out of any contextual handling, while an error in a `@throws` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers). +
#### Operation errors + `@handles` decorator @@ -1086,6 +1095,8 @@ This distinction ensures that the proposal remains flexible and applicable to a ## Additional Considerations + + ### Optional: Adding Context Modifiers to `@error` As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. From 8dfc2556fe9d3ca1b9e78f69307e86e43108a35d Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Thu, 22 May 2025 16:21:09 -0700 Subject: [PATCH 11/18] updates from feedback --- .../model-property-error-handling.md | 191 +++++++++--------- 1 file changed, 99 insertions(+), 92 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index fe910ae9654..be22c7afc60 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -1,50 +1,59 @@ # Proposal: Expressing Model Property Error Handling -This proposal suggests adding two new decorators to the TypeSpec standard library: +This proposal introduces two decorators for the TypeSpec standard library: -- `@throws` decorator: This decorator is used to specify that the use of a model property may result in specific errors being produced. -- `@handles` decorator: This decorator is used to specify that an operation or property will handle certain types of errors, preventing them from being propagated. +- `@raises` decorator: Used to indicate that a model property may be associated with specific errors. +- `@handles` decorator: Used to indicate that an operation or property will handle certain types of errors, preventing them from being considered further. -It also proposes updating existing emitters to support these new decorators. +The proposal also recommends that new and existing emitters support these decorators for improved error documentation and code generation.
## Goals -1. Provide a way for TSP developers to document errors associated with a particular model property. -2. Provide spec emitters with information that can be used to update the set of operation errors based on the models in use, and the error handling of the operation. -3. Allow code emitters to generate code that expects and handles these errors appropriately. +1. Enable TypeSpec developers to document where errors may occur in model properties. +2. Provide emitters with information to update the set of [operation error](#operation-error)s based on the models in use and the error handling of the operation. +3. Allow code emitters to generate code that is aware of and can respond to these errors appropriately. + +
+ +## Terminology + +- **Operation error**: An error that is included in an operation's return type, or otherwise surfaced directly by the operation. [Operation error](#operation-error)s are part of the API contract and are explicitly documented as possible results of invoking the operation. + +
+ +1. Enable TypeSpec developers to document where errors may occur in model properties. +2. Provide emitters with information to update the set of [operation errors](#operation-error) based on the models in use and the error handling of the operation. +3. Allow code emitters to generate code that is aware of and can respond to these errors appropriately.
## Definitions -### `@throws` decorator +### `@raises` decorator ````typespec /** - * Specify that the use of this property may result in specific errors being produced. + * Indicates that the use of this property may be associated with specific errors. * - * @param errors The list of error models that may be produced when using this property. + * @param errors The list of error models that may be associated with this property. * * @example * * ```typespec * model User { - * @throws(NotFoundError, PermissionDeniedError, InvalidURLError) + * @raises(NotFoundError, PermissionDeniedError, InvalidURLError) * profilePictureUrl: string; * } * ``` */ -extern dec throws(target: ModelProperty, ...errors: Model[]); +extern dec raises(target: ModelProperty, ...errors: Model[]); ```` -The decorator can be applied to model properties. -It specifies that any operation or context using the decorated property may produce the listed errors. -This allows consumers to anticipate and handle these errors appropriately. +The `@raises` decorator is applied to model properties to document that certain errors may be associated with those properties. This provides valuable information for documentation and code generation, helping consumers and tools understand where errors may occur within a model. -The `errors` parameter is a list of models that represent the possible errors that can be thrown. -Each model must be decorated with the [`@error` decorator][error-decorator]. +The `errors` parameter is a list of models representing possible errors. Each error model must be decorated with the [`@error` decorator][error-decorator].
@@ -52,7 +61,7 @@ Each model must be decorated with the [`@error` decorator][error-decorator]. ````typespec /** - * Specify that this operation or model property will handle certain types of errors. + * Indicates that this operation or model property will handle certain types of errors. * * @param errors The list of error models that will be handled by this operation or model property. * @@ -75,11 +84,11 @@ It specifies that the operation or model property will handle the listed errors, The `errors` parameter is a list of models that represent the errors that will be handled by the operation or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. -For example, if a property handles an error internally, that error will not propagate to the operation's response type: +For example, if a property handles an error internally, that error will not propagate to the operation's return type: ```typespec model User { - @throws(InvalidURLError) + @raises(InvalidURLError) @handles(PermissionDeniedError) profilePictureUrl: string; } @@ -90,27 +99,26 @@ op getUser(@path id: string): User | GenericError; ``` In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. -However, the `InvalidURLError` is still propagated to the operation's response type. +However, the `InvalidURLError` is still propagated to the operation's return type. -#### Operation errors + `@throws` decorator +#### [Operation errors](#operation-error) + `@raises` decorator -The `@throws` decorator can also be used in conjunction with an operation's return type. -In the examples above, we are specifying that `getUser()` may return a `GenericError` in addition to the errors that may be produced by the `profilePictureUrl` property or any other property. +The `@raises` decorator can be used alongside an operation's return type. For example, `getUser()` may return a `GenericError` in its return type, in addition to errors that may be associated with properties like `profilePictureUrl`. -If an error type is specified in both the operation's return type and the `@throws` decorator, there is no conflict — the operation will include the error (once) in the list of possible errors. +If an error type is specified in both the operation's return type and the `@raises` decorator, the operation will include the error (once) in the list of possible errors. -Semantically, the distinction between a `@throws` decorator and the operation's return type is in where the error is communicated. -An error on a return type indicates that the error is somehow exposed directly in that return type. -An error specified with `@throws`, on the other hand, may appear in a different location depending on if or where the error is is specified in a `@handles` decorator. +Semantically, the distinction between a `@raises` decorator and the operation's return type is in where the error is communicated. +An error on a return type indicates that the error is somehow exposed directly in that response. +An error specified with `@raises`, on the other hand, may appear in a different location depending on if or where the error is specified in a `@handles` decorator. For instance, a bulk operation of some kind that includes the results of several sub-operations could communicate errors in a few different ways. -One way would be for each of the operations in the bulk set to provide its error value as its specific return type — as indicated by an error present in the return type. -Another might be for the bulk operation to aggregate all of the errors that occurred in the sub-operations and communicate them somewhere in its own response, which could be accomplished by the sub-operations using the `@throws` decorator and the bulk operation using the `@handles` decorator. -Essentially, an error in a return type is opted out of any contextual handling, while an error in a `@throws` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers). +One way would be for each of the operations in the bulk set to provide its error value as its specific return type — as indicated by an error present in the return type.∆ +Another might be for the bulk operation to aggregate all the errors that occurred in the sub-operations and communicate them somewhere in its own response, which could be accomplished by the sub-operations using the `@raises` decorator and the bulk operation using the `@handles` decorator. +Essentially, an error in a return type is opted out of any contextual handling, while an error in a `@raises` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers).
-#### Operation errors + `@handles` decorator +#### [Operation errors](#operation-error) + `@handles` decorator It is possible, and valid, that an operation both `@handles` an error and also has a return type that includes that error. In this case, the operation _will_ include the error in the list of possible errors for the operation. @@ -126,14 +134,13 @@ Semantically, this indicates that the operation will handle the `InvalidURLError This becomes important when considering error inheritance. -#### `@throws` + `@handles` decorator +#### `@raises` + `@handles` decorator -Similarly, model properties may have one or more error types defined in both their `@throws` decorator and the `@handles` decorator. -In this case, the error is still considered to be throwable by the model property. +Model properties may have one or more error types defined in both their `@raises` decorator and the `@handles` decorator. In this case, the error is still considered possible at that property. Code emitters should treat `@raises` as taking precedence for code generation and documentation. ```typespec model User { - @throws(InvalidURLError) + @raises(InvalidURLError) @handles(PermissionDeniedError, InvalidURLError) profilePictureUrl: string; } @@ -170,25 +177,25 @@ op getUser(@path id: string): User | GenericError; Now, any errors thrown by any of the model properties used in the operation will not be added to the operation's error output if they extend from `GenericError`. -This inheritance does _not_ apply to the `@throws` decorator. -If a property is decorated with `@throws(GenericError)`, it is not considered to be decorated with `@throws(NotFoundError)` or `@throws(PermissionDeniedError)`, even though those errors extend from `GenericError`. +This inheritance does _not_ apply to the `@raises` decorator. +If a property is decorated with `@raises(GenericError)`, it is not considered to be decorated with `@raises(NotFoundError)` or `@raises(PermissionDeniedError)`, even though those errors extend from `GenericError`. -Conversely, if a property is decorated with `@throws(NotFoundError)`, it is not considered to be decorated with `@throws(GenericError)`. +Conversely, if a property is decorated with `@raises(NotFoundError)`, it is not considered to be decorated with `@raises(GenericError)`. -This is meant to align with exception semantics common among many languages, where a specific exception type must be specified when thrown but a class or category of exceptions can be caught. +This approach aligns with the idea that error documentation should be explicit about which errors may occur at a given property, while allowing for more flexible handling in `@handles`.
## Implementations and Use Cases -Below we list some proposed implementations in various emitter targets. These are meant to be illustrative of the effects of the `@throws` and `@handles` decorators, and are not proposing any of the specific syntax or implementation shown below. +Below we list some proposed implementations in various emitter targets. These are meant to be illustrative of the effects of the `@raises` and `@handles` decorators, and are not proposing any of the specific syntax or implementation shown below. ### HTTP/REST/OpenAPI -In a typical HTTP/REST API where operations are represented by endpoints, the `@throws` decorator can provide more accurate return types for operations that contain properties that may fail. +In a typical HTTP/REST API where operations are represented by endpoints, the `@raises` decorator can provide more accurate return types for operations that contain properties that may fail. In a larger API, it may be quite difficult to track all of the errors that can occur within an operation when the errors can be generated by many different layers of an API stack. -The `@throws` decorator helps give the developer a more complete view of the errors that an operation can produce. +The `@raises` decorator helps give the developer a more complete view of the errors that an operation can produce. Let's say we have this definition of models: @@ -268,15 +275,15 @@ paths:
-#### Using `@throws` decorator +#### Using `@raises` decorator -With the `@throws` decorator, we can specify that the `profilePictureUrl` property may produce errors when accessed: +With the `@raises` decorator, we can specify that the `profilePictureUrl` property may produce errors when accessed: ```typespec model User { @key id: string; - @throws(NotFoundError, PermissionDeniedError, InvalidURLError) + @raises(NotFoundError, PermissionDeniedError, InvalidURLError) profilePictureUrl: string; } ``` @@ -396,21 +403,21 @@ paths:
This is not limited to the `profilePictureUrl` property. -Any property that is decorated with `@throws(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation. +Any property that is decorated with `@raises(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation.
### GraphQL In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]. -The `@throws` decorator can be used to document which errors may occur when resolving a specific field. -For example, a field decorated with `@throws` could generate GraphQL schema documentation indicating the possible errors. +The `@raises` decorator can be used to document which errors may occur when resolving a specific field. +For example, a field decorated with `@raises` could generate GraphQL schema documentation indicating the possible errors. Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], where errors are included in the possible value of a field using union types. -In this case, the `@throws` decorator can be used to specify which errors must be included in that union type. +In this case, the `@raises` decorator can be used to specify which errors must be included in that union type. The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. -These decorators can be used to customize how errors in a `@throws` decorator are emitted in the GraphQL schema. +These decorators can be used to customize how errors in a `@raises` decorator are emitted in the GraphQL schema. For example, a `@propagate` decorator could be used to indicate that an error type, if produced, should be propagated to parent fields. In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. @@ -421,7 +428,7 @@ This allows a GraphQL schema to opt-in to using this pattern for specific errors The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. Specifying an error in the `@handles` decorator will: -- omit the error from the union response type, if the error has the `@asData` decorator. +- omit the error from the union return type, if the error has the `@asData` decorator. - prevent the error from triggering non-nullability of the field type, if the error has the `@propagate` decorator. The field may still be marked non-null through other errors or other means. @@ -483,7 +490,7 @@ op markAsSeen(seen: boolean): boolean | RaceConditionError; @GraphQL.operationFields(markAsSeen) model ActivityEntry { - @throws(PermissionDeniedError) ipAddress?: string; + @raises(PermissionDeniedError) ipAddress?: string; } // In GraphQL, fields can take arguments. @@ -493,10 +500,10 @@ model ActivityEntry { @GraphQL.operationFields(followers) model User { - @throws(NotFoundError, PermissionDeniedError) profilePictureUrl: string; + @raises(NotFoundError, PermissionDeniedError) profilePictureUrl: string; @doc("A log of the user's activity") - @throws(UpstreamTimeoutError) activity: ActivityEntry[]; + @raises(UpstreamTimeoutError) activity: ActivityEntry[]; } ``` @@ -551,7 +558,7 @@ union UserProfilePictureUrlResponse = type User { """ A log of the user's activity - * this field is non-null because it `@throws(UpstreamTimeoutError)` (which propagates) + * this field is non-null because it `@raises(UpstreamTimeoutError)` (which propagates) """ activity: [ActivityEntry!]! @@ -582,11 +589,11 @@ type ActivityEntry { ### Protocol Buffers (Protobuf) -Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. While Protobuf itself does not have a built-in concept of errors, the `@throws` and `@handles` decorators can be used to model and document errors in TypeSpec, which can then be translated into Protobuf-compatible patterns. Different patterns for communicating errors in Protobuf can be expressed using additional TypeSpec decorators. +Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. While Protobuf itself does not have a built-in concept of errors, the `@raises` and `@handles` decorators can be used to model and document errors in TypeSpec, which can then be translated into Protobuf-compatible patterns. Different patterns for communicating errors in Protobuf can be expressed using additional TypeSpec decorators. -#### Using `@throws` with Protobuf +#### Using `@raises` with Protobuf -The `@throws` decorator can be used to specify errors that may occur when accessing a property. These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. +The `@raises` decorator can be used to specify errors that may occur when accessing a property. These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. For example: @@ -606,7 +613,7 @@ model PermissionDeniedError { } model User { - @throws(NotFoundError, PermissionDeniedError) + @raises(NotFoundError, PermissionDeniedError) profilePictureUrl: string; } @@ -673,7 +680,7 @@ model NotFoundError extends Error { } model User { - @throws(NotFoundError) profilePictureUrl: string; + @raises(NotFoundError) profilePictureUrl: string; } @route("/user/{id}") @@ -719,11 +726,11 @@ message GetUserRequest { ### Apache Thrift -Apache Thrift supports defining exceptions as part of its IDL (Interface Definition Language), which makes it well-suited for modeling error using the `@throws` and `@handles` decorators. +Apache Thrift supports defining exceptions as part of its IDL (Interface Definition Language), which makes it well-suited for modeling error using the `@raises` and `@handles` decorators. -#### Using `@throws` with Thrift +#### Using `@raises` with Thrift -Exceptions specified by `@throws` can be represented in Thrift by defining exception types and including them in the `throws` clause of a service method. +Exceptions specified by `@raises` can be represented in Thrift by defining exception types and including them in the `throws` clause of a service method. For example: @@ -741,7 +748,7 @@ model PermissionDeniedError { } model User { - @throws(NotFoundError, PermissionDeniedError) + @raises(NotFoundError, PermissionDeniedError) profilePictureUrl: string; } @@ -807,7 +814,7 @@ service UserService { ### Client libraries Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. -For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@throws` or operations decorated with `@handles`. +For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@raises` or operations decorated with `@handles`. #### Example: Kotlin @@ -825,7 +832,7 @@ sealed class Error { data class User( val id: String, - val profilePictureUrl: Result // Field with @throws decorator + val profilePictureUrl: Result // Field with @raises decorator ) fun getUser(id: String): Result { @@ -862,7 +869,7 @@ enum Error: Swift.Error { struct User { let id: String - let profilePictureUrl: Result // Field with @throws decorator + let profilePictureUrl: Result // Field with @raises decorator } func getUser(id: String) -> Result { @@ -903,7 +910,7 @@ case object NotFound extends Error case object PermissionDenied extends Error case object InvalidUrl extends Error -case class User(id: String, profilePictureUrl: Either[Error, String]) // Field with @throws decorator +case class User(id: String, profilePictureUrl: Either[Error, String]) // Field with @raises decorator def getUser(id: String): Either[Error, User] = { // Operation with @handles decorator @@ -951,30 +958,30 @@ Here, the server explicitly handles errors when resolving the `profile_picture_u ## Use in request input -The `@throws` and `@handles` decorators apply equally to input as they do to output. +The `@raises` and `@handles` decorators apply equally to input as they do to output. Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output.
-### `@throws` for Input Validation Errors +### `@raises` for Input Validation Errors -When applied to input properties, the `@throws` decorator specifies the errors that may occur during the validation or processing of client-provided data. +When applied to input properties, the `@raises` decorator specifies the errors that may occur during the validation or processing of client-provided data. For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: ```typespec model CreateUserRequest { - @throws(InvalidEmailError, MissingFieldError) + @raises(InvalidEmailError, MissingFieldError) email: string; - @throws(InvalidPasswordError) + @raises(InvalidPasswordError) password: string; } ``` These errors are generated by the server in response to invalid or incomplete input provided by the client. This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. -However, the propagation of errors from model properties to the operation's response type works the same way for input as it does for output. +However, the propagation of errors from model properties to the operation's return type works the same way for input as it does for output.
@@ -996,8 +1003,8 @@ This behavior mirrors how `@handles` is used for output errors, allowing develop ### Error Propagation for Input -Errors specified in `@throws` on input properties propagate to operations unless explicitly handled with `@handles`. -For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error response types because they are specified in the `CreateUserRequest` model: +Errors specified in `@raises` on input properties propagate to operations unless explicitly handled with `@handles`. +For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error return types because they are specified in the `CreateUserRequest` model: ```typespec @route("/user") @@ -1057,7 +1064,7 @@ model NotFoundError { op getUser(id: string): User | NotFoundError; ``` -The `@throws` decorator is different from the return type of operations in that it is used to document errors that may occur when accessing a property. +The `@raises` decorator is different from the return type of operations in that it is used to document errors that may occur when accessing a property. This distinction is useful when a property itself may inherently produce errors, regardless of the operation in which it is used. For example, accessing a property that requires a network fetch or a permission check may result in errors. @@ -1076,7 +1083,7 @@ The fact that `NotFoundError` and `PermissionDeniedError` use the `@error` decor ```typespec model User { - @throws(NotFoundError, PermissionDeniedError) + @raises(NotFoundError, PermissionDeniedError) profilePictureUrl: string; } ``` @@ -1086,7 +1093,7 @@ Typically, this means that the property does not _have_ a value in that scenario ## Summary -The `@throws` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. +The `@raises` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. While the mechanics of these decorators are identical for input and output, the use cases differ slightly. Input errors are generated by the server in response to invalid or incomplete client-provided data, whereas output errors are typically generated by the server's internal logic or data access operations. This distinction ensures that the proposal remains flexible and applicable to a wide range of error-handling scenarios. @@ -1103,7 +1110,7 @@ As an optional enhancement, we propose extending the `@error` decorator to inclu This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. -**This does not change the core mechanics of the `@throws` and `@handles` decorators, and the proposal for those decorators remains unchanged whether or not this enhancement is adopted.** +**This does not change the core mechanics of the `@raises` and `@handles` decorators, and the proposal for those decorators remains unchanged whether or not this enhancement is adopted.** ### Proposed Definition @@ -1166,7 +1173,7 @@ model User { @key id: string; @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) - @throws(InvalidEmailError) + @raises(InvalidEmailError) email: string; } @@ -1208,15 +1215,15 @@ Just as is true for visibility, if no context is specified, the error model [wil There are a number of ways to modify the visibility of a model or operation. Context modifiers, as applied to errors, will follow the same rules as they do for visibility. -For example, use of the [`@parameterVisibility`][parameter-visibility] or [`@returnTypeVisibility`][return-type-visibility] decorators will modify the visibility of the error model in the same way as it does for parameters. That is, the properties of a model used as a parameter will apply their `@throws` errors based on the visibility of parameters. The properties of a model used as a return type will apply their `@throws` errors based on the visibility of the return type. +For example, use of the [`@parameterVisibility`][parameter-visibility] or [`@returnTypeVisibility`][return-type-visibility] decorators will modify the visibility of the error model in the same way as it does for parameters. That is, the properties of a model used as a parameter will apply their `@raises` errors based on the visibility of parameters. The properties of a model used as a return type will apply their `@raises` errors based on the visibility of the return type. This also means that decorators which apply implicit visibility, such as [`@post`][post-decorator] or [`@put`][put-decorator], will apply the implicit visibility of the operation to the error model. Any other modification of visibility including visibility filters, custom context classes, et. al. should affect errors in the same way as they affect model properties. -#### Rejected alternative: Context modifiers on `@throws` and `@handles` +#### Rejected alternative: Context modifiers on `@raises` and `@handles` -An alternative to adding context modifiers to the `@error` decorator is to add them to the `@throws` and `@handles` decorators. +An alternative to adding context modifiers to the `@error` decorator is to add them to the `@raises` and `@handles` decorators. This would allow developers to specify the context in which an error applies model property by model property, rather than applying to an error model everywhere it appears. @@ -1227,16 +1234,16 @@ model User { @key id: string; @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) - @throws([InvalidEmailError], [Lifecycle.Create, Lifecycle.Update]) + @raises([InvalidEmailError], [Lifecycle.Create, Lifecycle.Update]) email: string; } ``` -While this approach does allow for finer granularity in specifying the context in which an error applies, it also adds complexity to the `@throws` and `@handles` decorators — and complexity for the developer to reason about the context in which an error applies. +While this approach does allow for finer granularity in specifying the context in which an error applies, it also adds complexity to the `@raises` and `@handles` decorators — and complexity for the developer to reason about the context in which an error applies. Applying context modifiers to the `@error` decorator abstracts the concerns of context away from any particular field or operation, so the developer does not always need to be considering it. It seems fairly intuitive for a developer to specify that an `InvalidParametersError` would only apply in input contexts, while a `PermissionDeniedError` would only apply in output contexts. -If context modifiers are specified on the `@throws` and `@handles` decorators, it is likely that the developer forgets to add all of the relevant lifecycle modifiers in some cases. +If context modifiers are specified on the `@raises` and `@handles` decorators, it is likely that the developer forgets to add all of the relevant lifecycle modifiers in some cases. This would result in operations insufficiently specifying errors, leading to clients receiving errors that they do not expect from the spec. By contrast, adding context modifiers to the `@error` decorator is more likely to add errors in more contexts than are needed; while not ideal, specifying extra errors in the spec that will never be returned is less problematic than omitting errors that will be. @@ -1246,8 +1253,8 @@ Indeed, there's no guarantee that _any_ error specified ever actually will be. ### Identifying Unused Error Handlers -TypeSpec only knows, and can only reason about, errors that are specified in a `@throws` decorator. -If an error is specified in a `@handles` decorator but not in any `@throws` decorator of all the model properties that are part of that property or operation, the TypeSpec compiler will not be able to determine whether the error is actually used. +TypeSpec only knows, and can only reason about, errors that are specified in a `@raises` decorator. +If an error is specified in a `@handles` decorator but not in any `@raises` decorator of all the model properties that are part of that property or operation, the TypeSpec compiler will not be able to determine whether the error is actually used. To help developers make that determination, the TypeSpec compiler will issue a warning when this scenario occurs. If the developer determines that the error _is_ thrown outside of the context of TypeSpec, they can use the standard [`# suppress` directive][suppress-directive] to suppress the warning. @@ -1270,7 +1277,7 @@ model PermissionDeniedError { } model User { - @throws(NotFoundError) + @raises(NotFoundError) profilePictureUrl: string; } @@ -1281,7 +1288,7 @@ op getUser(@path id: string): User; ``` In this example, the `getUser` operation specifies that it handles `PermissionDeniedError` using the `@handles` decorator. -However, none of the properties or operations used in `getUser` (in this case, just the `User.profilePictureUrl` property) specify `PermissionDeniedError` in their `@throws` decorators. +However, none of the properties or operations used in `getUser` (in this case, just the `User.profilePictureUrl` property) specify `PermissionDeniedError` in their `@raises` decorators. As a result, the TypeSpec compiler will issue a warning. From 81de282a68e169b2a5aec22090546c6a45c52537 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Thu, 22 May 2025 16:35:24 -0700 Subject: [PATCH 12/18] attempt to make things less HTTP/OpenAPI specific --- .../model-property-error-handling.md | 153 ++++++++---------- 1 file changed, 69 insertions(+), 84 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index be22c7afc60..8798e82cbff 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -3,7 +3,7 @@ This proposal introduces two decorators for the TypeSpec standard library: - `@raises` decorator: Used to indicate that a model property may be associated with specific errors. -- `@handles` decorator: Used to indicate that an operation or property will handle certain types of errors, preventing them from being considered further. +- `@handles` decorator: Used to indicate that an [operation](#operation) or property will handle certain types of errors, preventing them from being considered further. The proposal also recommends that new and existing emitters support these decorators for improved error documentation and code generation. @@ -19,7 +19,11 @@ The proposal also recommends that new and existing emitters support these decora ## Terminology -- **Operation error**: An error that is included in an operation's return type, or otherwise surfaced directly by the operation. [Operation error](#operation-error)s are part of the API contract and are explicitly documented as possible results of invoking the operation. +- **Operation**: A [TypeSpec operation](https://typespec.io/docs/language-basics/operations/) which defines an action or function that can be performed. In protocol-specific contexts like HTTP/REST, [operations](#operation) often map to API endpoints. In other contexts like GraphQL, [operations](#operation) may map to queries, mutations, or resolvers. [Operations](#operation) are a core TypeSpec concept, not specific to any protocol. + +- **Return type**: The [return type of a TypeSpec operation](https://typespec.io/docs/language-basics/operations/#return-type) which defines what the [operation](#operation) returns when invoked. This is a core TypeSpec language concept and exists independently of protocol-specific mechanisms for returning data or errors. Different protocols may represent the [return type](#return-type) in different ways (HTTP response bodies, GraphQL field values, etc.). + +- **Operation error**: An error that is included in an [operation](#operation)'s [return type](#return-type), or otherwise surfaced directly by the operation. [Operation error](#operation-error)s are part of the API contract and are explicitly documented as possible results of invoking the operation.
@@ -61,9 +65,9 @@ The `errors` parameter is a list of models representing possible errors. Each er ````typespec /** - * Indicates that this operation or model property will handle certain types of errors. + * Indicates that this [operation](#operation) or model property will handle certain types of errors. * - * @param errors The list of error models that will be handled by this operation or model property. + * @param errors The list of error models that will be handled by this [operation](#operation) or model property. * * @example * @@ -78,13 +82,13 @@ The `errors` parameter is a list of models representing possible errors. Each er extern dec handles(target: Operation | ModelProperty, ...errors: Model[]); ```` -The decorator can be applied to operations or model properties. -It specifies that the operation or model property will handle the listed errors, preventing them from being propagated to the client. +The decorator can be applied to [operations](#operation) or model properties. +It specifies that the [operation](#operation) or model property will handle the listed errors, preventing them from being propagated to the client. -The `errors` parameter is a list of models that represent the errors that will be handled by the operation or model property. +The `errors` parameter is a list of models that represent the errors that will be handled by the [operation](#operation) or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. -For example, if a property handles an error internally, that error will not propagate to the operation's return type: +For example, if a property handles an error internally, that error will not propagate to the [operation](#operation)'s [return type](#return-type): ```typespec model User { @@ -93,44 +97,40 @@ model User { profilePictureUrl: string; } -@route("/user/{id}") -@get -op getUser(@path id: string): User | GenericError; +op getUser(id: string): User | GenericError; ``` In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. -However, the `InvalidURLError` is still propagated to the operation's return type. +However, the `InvalidURLError` is still propagated to the [operation](#operation)'s [return type](#return-type). #### [Operation errors](#operation-error) + `@raises` decorator -The `@raises` decorator can be used alongside an operation's return type. For example, `getUser()` may return a `GenericError` in its return type, in addition to errors that may be associated with properties like `profilePictureUrl`. +The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). For example, `getUser()` may return a `GenericError` in its [return type](#return-type), in addition to errors that may be associated with properties like `profilePictureUrl`. -If an error type is specified in both the operation's return type and the `@raises` decorator, the operation will include the error (once) in the list of possible errors. +If an error type is specified in both the [operation](#operation)'s [return type](#return-type) and the `@raises` decorator, the [operation](#operation) will include the error (once) in the list of possible errors. -Semantically, the distinction between a `@raises` decorator and the operation's return type is in where the error is communicated. -An error on a return type indicates that the error is somehow exposed directly in that response. +Semantically, the distinction between a `@raises` decorator and the [operation](#operation)'s [return type](#return-type) is in where the error is communicated. +An error on a [return type](#return-type) indicates that the error is somehow exposed directly in that response. An error specified with `@raises`, on the other hand, may appear in a different location depending on if or where the error is specified in a `@handles` decorator. -For instance, a bulk operation of some kind that includes the results of several sub-operations could communicate errors in a few different ways. -One way would be for each of the operations in the bulk set to provide its error value as its specific return type — as indicated by an error present in the return type.∆ -Another might be for the bulk operation to aggregate all the errors that occurred in the sub-operations and communicate them somewhere in its own response, which could be accomplished by the sub-operations using the `@raises` decorator and the bulk operation using the `@handles` decorator. -Essentially, an error in a return type is opted out of any contextual handling, while an error in a `@raises` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers). +For instance, a bulk [operation](#operation) of some kind that includes the results of several sub-[operations](#operation) could communicate errors in a few different ways. +One way would be for each of the [operations](#operation) in the bulk set to provide its error value as its specific [return type](#return-type) — as indicated by an error present in the [return type](#return-type).∆ +Another might be for the bulk [operation](#operation) to aggregate all the errors that occurred in the sub-[operations](#operation) and communicate them somewhere in its own response, which could be accomplished by the sub-[operations](#operation) using the `@raises` decorator and the bulk [operation](#operation) using the `@handles` decorator. +Essentially, an error in a [return type](#return-type) is opted out of any contextual handling, while an error in a `@raises` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers).
#### [Operation errors](#operation-error) + `@handles` decorator -It is possible, and valid, that an operation both `@handles` an error and also has a return type that includes that error. -In this case, the operation _will_ include the error in the list of possible errors for the operation. +It is possible, and valid, that an [operation](#operation) both `@handles` an error and also has a [return type](#return-type) that includes that error. +In this case, the [operation](#operation) _will_ include the error in the list of possible errors for the operation. ```typespec -@route("/user/{id}") -@get @handles(InvalidURLError) -op getUser(@path id: string): User | InvalidURLError | GenericError; +op getUser(id: string): User | InvalidURLError | GenericError; ``` -Semantically, this indicates that the operation will handle the `InvalidURLError` error when produced by a model property, but that the operation itself may also return that error, outside the context of a model property. +Semantically, this indicates that the [operation](#operation) will handle the `InvalidURLError` error when produced by a model property, but that the [operation](#operation) itself may also return that error, outside the context of a model property. This becomes important when considering error inheritance. @@ -160,22 +160,16 @@ model GenericError { } @error -model NotFoundError extends GenericError { - @statusCode _: 404; -} +model NotFoundError extends GenericError {} @error -model PermissionDeniedError extends GenericError { - @statusCode _: 403; -} +model PermissionDeniedError extends GenericError {} -@route("/user/{id}") -@get @handles(GenericError) -op getUser(@path id: string): User | GenericError; +op getUser(id: string): User | GenericError; ``` -Now, any errors thrown by any of the model properties used in the operation will not be added to the operation's error output if they extend from `GenericError`. +Now, any errors thrown by any of the model properties used in the [operation](#operation) will not be added to the [operation](#operation)'s error output if they extend from `GenericError`. This inheritance does _not_ apply to the `@raises` decorator. If a property is decorated with `@raises(GenericError)`, it is not considered to be decorated with `@raises(NotFoundError)` or `@raises(PermissionDeniedError)`, even though those errors extend from `GenericError`. @@ -192,10 +186,10 @@ Below we list some proposed implementations in various emitter targets. These ar ### HTTP/REST/OpenAPI -In a typical HTTP/REST API where operations are represented by endpoints, the `@raises` decorator can provide more accurate return types for operations that contain properties that may fail. +In a typical HTTP/REST API where [operations](#operation) are represented by endpoints, the `@raises` decorator can provide more accurate [return type](#return-type)s for [operations](#operation) that contain properties that may fail. -In a larger API, it may be quite difficult to track all of the errors that can occur within an operation when the errors can be generated by many different layers of an API stack. -The `@raises` decorator helps give the developer a more complete view of the errors that an operation can produce. +In a larger API, it may be quite difficult to track all of the errors that can occur within an [operation](#operation) when the errors can be generated by many different layers of an API stack. +The `@raises` decorator helps give the developer a more complete view of the errors that an [operation](#operation) can produce. Let's say we have this definition of models: @@ -233,7 +227,7 @@ model User {
-Now we define an operation that uses the `User` model: +Now we define an [operation](#operation) that uses the `User` model: ```typespec @route("/user/{id}") @@ -338,7 +332,7 @@ paths: -The definition of `getUser()` has not changed, but it is now emitted as if the return type was +The definition of `getUser()` has not changed, but it is now emitted as if the [return type](#return-type) was ```typespec User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; @@ -348,8 +342,8 @@ User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; #### Using `@handles` decorator -Perhaps our `getUser()` operation is designed to handle the `InvalidURLError` error, while other operations may not do so. -We can use the `@handles` decorator to specify that this operation will handle that error: +Perhaps our `getUser()` [operation](#operation) is designed to handle the `InvalidURLError` error, while other [operations](#operation) may not do so. +We can use the `@handles` decorator to specify that this [operation](#operation) will handle that error: ```typespec @route("/user/{id}") @@ -403,7 +397,7 @@ paths: This is not limited to the `profilePictureUrl` property. -Any property that is decorated with `@raises(InvalidURLError)` and is used in the `getUser()` operation will no longer add `InvalidURLError` to the list of possible errors for the operation. +Any property that is decorated with `@raises(InvalidURLError)` and is used in the `getUser()` [operation](#operation) will no longer add `InvalidURLError` to the list of possible errors for the operation.
@@ -423,12 +417,12 @@ For example, a `@propagate` decorator could be used to indicate that an error ty In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. A `@asData` decorator could be used to indicate that an error type should be included in the ["errors as data" pattern][errors-as-data]. -This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. +This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. This avoids changing the shape of the TypeSpec API description to serve the needs of a specific protocol. The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. Specifying an error in the `@handles` decorator will: -- omit the error from the union return type, if the error has the `@asData` decorator. +- omit the error from the union [return type](#return-type), if the error has the `@asData` decorator. - prevent the error from triggering non-nullability of the field type, if the error has the `@propagate` decorator. The field may still be marked non-null through other errors or other means. @@ -494,7 +488,7 @@ model ActivityEntry { } // In GraphQL, fields can take arguments. -// These are specified like operations in TypeSpec. +// These are specified like [operations](#operation) in TypeSpec. @doc("Users following this user") @handles(RaceConditionError) op followers(type?: string): User[]; @@ -577,7 +571,7 @@ type ActivityEntry { """ Mark this entry as seen - * this field is non-null because it has RaceConditionError (which propagates) in its return type + * this field is non-null because it has RaceConditionError (which propagates) in its [return type](#return-type) """ markAsSeen(seen: Boolean!): Boolean! } @@ -663,7 +657,7 @@ message GetUserRequest { #### Using gRPC Status Codes When using Protobuf with gRPC, errors are often communicated using gRPC's built-in status codes and error details. -These could be expressed in TypeSpec using a `@statusCode` decorator from a gRPC library, along with a generic `Error` model in the operation's return type. +These could be expressed in TypeSpec using a `@statusCode` decorator from a gRPC library, along with a generic `Error` model in the [operation](#operation)'s [return type](#return-type).
Click to collapse @@ -788,7 +782,7 @@ service UserService { #### Using `@handles` with Thrift -The `@handles` decorator can be used to specify which exceptions are handled internally by an operation or property. In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. +The `@handles` decorator can be used to specify which exceptions are handled internally by an [operation](#operation) or property. In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. For example: @@ -813,8 +807,8 @@ service UserService { ### Client libraries -Client libraries should leverage language-specific constructs to represent fields or operations that may produce errors. -For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@raises` or operations decorated with `@handles`. +Client libraries should leverage language-specific constructs to represent fields or [operations](#operation) that may produce errors. +For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@raises` or [operations](#operation) decorated with `@handles`. #### Example: Kotlin @@ -855,7 +849,7 @@ This approach ensures that clients handle errors in a type-safe and idiomatic wa #### Example: Swift -In Swift, the `Result` type can be used to represent fields or operations that may fail. +In Swift, the `Result` type can be used to represent fields or [operations](#operation) that may fail. For example:
Click to collapse @@ -895,7 +889,7 @@ This approach ensures that clients handle errors in a type-safe and idiomatic wa ### Server libraries Server libraries should generate code that includes appropriate error handling stubs. -For example, in languages with an error monad or result monad, these constructs should be used to represent fields or operations that may produce errors. +For example, in languages with an error monad or result monad, these constructs should be used to represent fields or [operations](#operation) that may produce errors. This allows server implementations to handle errors explicitly and propagate them as needed. #### Example: Scala @@ -981,18 +975,16 @@ model CreateUserRequest { These errors are generated by the server in response to invalid or incomplete input provided by the client. This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. -However, the propagation of errors from model properties to the operation's return type works the same way for input as it does for output. +However, the propagation of errors from model properties to the [operation](#operation)'s [return type](#return-type) works the same way for input as it does for output.
### `@handles` for Input-Level Error Handling -The `@handles` decorator can be used to specify which input-related errors are handled by the operation itself, preventing them from being propagated to the client. -For example, an operation to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: +The `@handles` decorator can be used to specify which input-related errors are handled by the [operation](#operation) itself, preventing them from being propagated to the client. +For example, an [operation](#operation) to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: ```typespec -@route("/user") -@post @handles(InvalidEmailError) op createUser(request: CreateUserRequest): User | GenericError; ``` @@ -1003,12 +995,10 @@ This behavior mirrors how `@handles` is used for output errors, allowing develop ### Error Propagation for Input -Errors specified in `@raises` on input properties propagate to operations unless explicitly handled with `@handles`. -For example, the following operation automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error return types because they are specified in the `CreateUserRequest` model: +Errors specified in `@raises` on input properties propagate to [operations](#operation) unless explicitly handled with `@handles`. +For example, the following [operation](#operation) automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error [return type](#return-type)s because they are specified in the `CreateUserRequest` model: ```typespec -@route("/user") -@post op createUser(request: CreateUserRequest): | User | InvalidEmailError @@ -1018,7 +1008,7 @@ op createUser(request: CreateUserRequest): ``` This ensures consistency between input and output error modeling. -By default, errors propagate from input properties to operations, but operations can override this behavior with `@handles`. +By default, errors propagate from input properties to operations, but [operations](#operation) can override this behavior with `@handles`.
@@ -1050,7 +1040,7 @@ This is consistent with how protocol-specific metadata is applied to output erro ### Mimic error handling in operations -TypeSpec [operations][operations] allow for specifying possible errors that the operation may produce via the [operation's return type][operations-return-type]. +TypeSpec [operations][operations] allow for specifying possible errors that the [operation](#operation) may produce via the [operation's return type][operations-return-type]. The standard pattern is to use a union type that includes the models representing the errors, which have been decorated with the [`@error` decorator][error-decorator]. For example: @@ -1064,12 +1054,12 @@ model NotFoundError { op getUser(id: string): User | NotFoundError; ``` -The `@raises` decorator is different from the return type of operations in that it is used to document errors that may occur when accessing a property. +The `@raises` decorator is different from the [return type](#return-type) of [operations](#operation) in that it is used to document errors that may occur when accessing a property. -This distinction is useful when a property itself may inherently produce errors, regardless of the operation in which it is used. +This distinction is useful when a property itself may inherently produce errors, regardless of the [operation](#operation) in which it is used. For example, accessing a property that requires a network fetch or a permission check may result in errors. -Using a union type, as operations do, does not allow for the same error semantic in model properties. +Using a union type, as [operations](#operation) do, does not allow for the same error semantic in model properties. Instead, such a type would be indicative of possible types for the property's _value_: ```typespec @@ -1118,7 +1108,7 @@ The `@error` decorator would accept an optional argument specifying one or more ````typespec /** - * Specify that this model is an error type. Operations return error types when the operation has failed. + * Specify that this model is an error type. Operations return error types when the [operation](#operation) has failed. * * @param contexts The list of contexts in which this error applies. This can be used to indicate whether the error is relevant for input, output, or both. * @@ -1177,11 +1167,11 @@ model User { email: string; } -@get op getUser(id: string): User | UserNotFound; // will not include InvalidEmailError, does return email field +op getUser(id: string): User | UserNotFound; // will not include InvalidEmailError, does return email field -@post op createUser(...User): User; // will include InvalidEmailError, does return email field +op createUser(...User): User; // will include InvalidEmailError, does return email field -@delete op deleteUser(id: string): User; // does not include InvalidEmailError, does not return email field +op deleteUser(id: string): User; // does not include InvalidEmailError, does not return email field ```
@@ -1191,9 +1181,7 @@ model User { By default, errors with `Lifecycle.Read` are included when the model is used in an output context. ```typespec -@route("/user/{id}") -@get -op getUser(@path id: string): User | PermissionDeniedError | GenericError; +op getUser(id: string): User | PermissionDeniedError | GenericError; ``` ##### Both Contexts @@ -1215,9 +1203,9 @@ Just as is true for visibility, if no context is specified, the error model [wil There are a number of ways to modify the visibility of a model or operation. Context modifiers, as applied to errors, will follow the same rules as they do for visibility. -For example, use of the [`@parameterVisibility`][parameter-visibility] or [`@returnTypeVisibility`][return-type-visibility] decorators will modify the visibility of the error model in the same way as it does for parameters. That is, the properties of a model used as a parameter will apply their `@raises` errors based on the visibility of parameters. The properties of a model used as a return type will apply their `@raises` errors based on the visibility of the return type. +For example, use of the [`@parameterVisibility`][parameter-visibility] or [`@returnTypeVisibility`][return-type-visibility] decorators will modify the visibility of the error model in the same way as it does for parameters. That is, the properties of a model used as a parameter will apply their `@raises` errors based on the visibility of parameters. The properties of a model used as a [return type](#return-type) will apply their `@raises` errors based on the visibility of the [return type](#return-type). -This also means that decorators which apply implicit visibility, such as [`@post`][post-decorator] or [`@put`][put-decorator], will apply the implicit visibility of the operation to the error model. +This also means that decorators which apply implicit visibility, such as [`@post`][post-decorator] or [`@put`][put-decorator], will apply the implicit visibility of the [operation](#operation) to the error model. Any other modification of visibility including visibility filters, custom context classes, et. al. should affect errors in the same way as they affect model properties. @@ -1244,7 +1232,7 @@ Applying context modifiers to the `@error` decorator abstracts the concerns of c It seems fairly intuitive for a developer to specify that an `InvalidParametersError` would only apply in input contexts, while a `PermissionDeniedError` would only apply in output contexts. If context modifiers are specified on the `@raises` and `@handles` decorators, it is likely that the developer forgets to add all of the relevant lifecycle modifiers in some cases. -This would result in operations insufficiently specifying errors, leading to clients receiving errors that they do not expect from the spec. +This would result in [operations](#operation) insufficiently specifying errors, leading to clients receiving errors that they do not expect from the spec. By contrast, adding context modifiers to the `@error` decorator is more likely to add errors in more contexts than are needed; while not ideal, specifying extra errors in the spec that will never be returned is less problematic than omitting errors that will be. Indeed, there's no guarantee that _any_ error specified ever actually will be. @@ -1281,14 +1269,11 @@ model User { profilePictureUrl: string; } -@route("/user/{id}") -@get -@handles(PermissionDeniedError) // warning -op getUser(@path id: string): User; +op getUser(id: string): User | NotFoundError; ``` -In this example, the `getUser` operation specifies that it handles `PermissionDeniedError` using the `@handles` decorator. -However, none of the properties or operations used in `getUser` (in this case, just the `User.profilePictureUrl` property) specify `PermissionDeniedError` in their `@raises` decorators. +In this example, the `getUser` [operation](#operation) specifies that it handles `PermissionDeniedError` using the `@handles` decorator. +However, none of the properties or [operations](#operation) used in `getUser` (in this case, just the `User.profilePictureUrl` property) specify `PermissionDeniedError` in their `@raises` decorators. As a result, the TypeSpec compiler will issue a warning. From c8611aebc3398e9c5e2391053838c2c373dbc412 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Thu, 5 Jun 2025 10:58:00 -0700 Subject: [PATCH 13/18] Rework definitions for more clarity with "protocol error" --- .../model-property-error-handling.md | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 8798e82cbff..ba8ac2e8b58 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -2,8 +2,8 @@ This proposal introduces two decorators for the TypeSpec standard library: -- `@raises` decorator: Used to indicate that a model property may be associated with specific errors. -- `@handles` decorator: Used to indicate that an [operation](#operation) or property will handle certain types of errors, preventing them from being considered further. +- [`@raises` decorator](#raises-decorator): Used to indicate that a model property may be associated with specific errors. +- [`@handles` decorator](#handles-decorator): Used to indicate that an [operation](#operation) or property will handle certain types of errors, preventing them from being considered further. The proposal also recommends that new and existing emitters support these decorators for improved error documentation and code generation. @@ -12,24 +12,23 @@ The proposal also recommends that new and existing emitters support these decora ## Goals 1. Enable TypeSpec developers to document where errors may occur in model properties. -2. Provide emitters with information to update the set of [operation error](#operation-error)s based on the models in use and the error handling of the operation. +2. Provide emitters with information to update the set of [operation errors](#operation-error) based on the models in use and the error handling of the operation. 3. Allow code emitters to generate code that is aware of and can respond to these errors appropriately.
## Terminology -- **Operation**: A [TypeSpec operation](https://typespec.io/docs/language-basics/operations/) which defines an action or function that can be performed. In protocol-specific contexts like HTTP/REST, [operations](#operation) often map to API endpoints. In other contexts like GraphQL, [operations](#operation) may map to queries, mutations, or resolvers. [Operations](#operation) are a core TypeSpec concept, not specific to any protocol. +These terms have specific meanings throughout the document, so we will define them here. -- **Return type**: The [return type of a TypeSpec operation](https://typespec.io/docs/language-basics/operations/#return-type) which defines what the [operation](#operation) returns when invoked. This is a core TypeSpec language concept and exists independently of protocol-specific mechanisms for returning data or errors. Different protocols may represent the [return type](#return-type) in different ways (HTTP response bodies, GraphQL field values, etc.). +- **Operation**: A [TypeSpec operation](https://typespec.io/docs/language-basics/operations/) which defines an action or function that can be performed. In protocol-specific contexts like HTTP/REST, [operations](#operation) often map to API endpoints. In other contexts like GraphQL, operations may map to queries, mutations, or resolvers. Operations are a core TypeSpec concept, not specific to any protocol. +- ``` -- **Operation error**: An error that is included in an [operation](#operation)'s [return type](#return-type), or otherwise surfaced directly by the operation. [Operation error](#operation-error)s are part of the API contract and are explicitly documented as possible results of invoking the operation. +- **Return type**: The [return type of a TypeSpec operation](https://typespec.io/docs/language-basics/operations/#return-type) which defines what the [operation](#operation) returns when invoked. This is a core TypeSpec language concept and exists independently of protocol-specific mechanisms for returning data or errors. Different protocols may represent the return type#return-type in different ways (HTTP response bodies, GraphQL field values, etc.). -
+- **Operation error**: An error that is specified in the [return type](#return-type) of an [operation](#operation) in TypeSpec. Operation errors are part of the API contract and are explicitly documented as possible results of invoking the operation. -1. Enable TypeSpec developers to document where errors may occur in model properties. -2. Provide emitters with information to update the set of [operation errors](#operation-error) based on the models in use and the error handling of the operation. -3. Allow code emitters to generate code that is aware of and can respond to these errors appropriately. +- **Protocol error**: An error expression specific to the protocol in which it is being expressed. Protocol errors may be the result of any number of sources, e.g. [operation errors](#operation-error), raised model property errors, or protocol processing errors. An [operation error](#operation-error) does not necessarily translate into a protocol error (it's up to the protocol emitter).
@@ -57,6 +56,8 @@ extern dec raises(target: ModelProperty, ...errors: Model[]); The `@raises` decorator is applied to model properties to document that certain errors may be associated with those properties. This provides valuable information for documentation and code generation, helping consumers and tools understand where errors may occur within a model. +Protocol emitters are expected to consider errors listed in `@raises` decorators when determining what [protocol errors](#protocol-error) should be expressed. + The `errors` parameter is a list of models representing possible errors. Each error model must be decorated with the [`@error` decorator][error-decorator].
@@ -65,9 +66,9 @@ The `errors` parameter is a list of models representing possible errors. Each er ````typespec /** - * Indicates that this [operation](#operation) or model property will handle certain types of errors. + * Indicates that this operation or model property will handle certain types of errors. * - * @param errors The list of error models that will be handled by this [operation](#operation) or model property. + * @param errors The list of error models that will be handled by this operation or model property. * * @example * @@ -83,26 +84,11 @@ extern dec handles(target: Operation | ModelProperty, ...errors: Model[]); ```` The decorator can be applied to [operations](#operation) or model properties. -It specifies that the [operation](#operation) or model property will handle the listed errors, preventing them from being propagated to the client. +It specifies that the [operation](#operation) or model property will handle the listed errors, preventing them from being expressed as [protocol errors](#protocol-error). The `errors` parameter is a list of models that represent the errors that will be handled by the [operation](#operation) or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. -For example, if a property handles an error internally, that error will not propagate to the [operation](#operation)'s [return type](#return-type): - -```typespec -model User { - @raises(InvalidURLError) - @handles(PermissionDeniedError) - profilePictureUrl: string; -} - -op getUser(id: string): User | GenericError; -``` - -In this case, the `PermissionDeniedError` is handled internally by the `profilePictureUrl` property and does not appear in the list of possible errors for the `getUser` operation. -However, the `InvalidURLError` is still propagated to the [operation](#operation)'s [return type](#return-type). - #### [Operation errors](#operation-error) + `@raises` decorator The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). For example, `getUser()` may return a `GenericError` in its [return type](#return-type), in addition to errors that may be associated with properties like `profilePictureUrl`. From aab83142afa560d625fe988e784fb0789a22ac86 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Thu, 5 Jun 2025 16:11:18 -0700 Subject: [PATCH 14/18] polish --- .../model-property-error-handling.md | 262 ++++++++++-------- 1 file changed, 149 insertions(+), 113 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index ba8ac2e8b58..9e7bde2a7ce 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -21,14 +21,18 @@ The proposal also recommends that new and existing emitters support these decora These terms have specific meanings throughout the document, so we will define them here. -- **Operation**: A [TypeSpec operation](https://typespec.io/docs/language-basics/operations/) which defines an action or function that can be performed. In protocol-specific contexts like HTTP/REST, [operations](#operation) often map to API endpoints. In other contexts like GraphQL, operations may map to queries, mutations, or resolvers. Operations are a core TypeSpec concept, not specific to any protocol. -- ``` +#### Operation +A [TypeSpec operation][typespec-operation] which defines an action or function that can be performed. In protocol-specific contexts like HTTP/REST, [operations](#operation) often map to API endpoints. In other contexts like GraphQL, operations may map to queries, mutations, or resolvers. Operations are a core TypeSpec concept, not specific to any protocol. -- **Return type**: The [return type of a TypeSpec operation](https://typespec.io/docs/language-basics/operations/#return-type) which defines what the [operation](#operation) returns when invoked. This is a core TypeSpec language concept and exists independently of protocol-specific mechanisms for returning data or errors. Different protocols may represent the return type#return-type in different ways (HTTP response bodies, GraphQL field values, etc.). +#### Return type +The [return type of a TypeSpec operation](https://typespec.io/docs/language-basics/operations/#return-type) which defines what the [operation](#operation) returns when invoked. This is a core TypeSpec language concept and exists independently of protocol-specific mechanisms for returning data or errors. Different protocols may represent the return type in different ways (HTTP response bodies, GraphQL field values, etc.). -- **Operation error**: An error that is specified in the [return type](#return-type) of an [operation](#operation) in TypeSpec. Operation errors are part of the API contract and are explicitly documented as possible results of invoking the operation. +#### Operation error +An error that is specified in the [return type](#return-type) of an [operation](#operation) in TypeSpec. Operation errors are part of the API contract and are explicitly documented as possible results of invoking the operation. -- **Protocol error**: An error expression specific to the protocol in which it is being expressed. Protocol errors may be the result of any number of sources, e.g. [operation errors](#operation-error), raised model property errors, or protocol processing errors. An [operation error](#operation-error) does not necessarily translate into a protocol error (it's up to the protocol emitter). + +#### Protocol error +An error expression specific to the protocol in which it is being expressed. Protocol errors may be the result of any number of sources, e.g. [operation errors](#operation-error), raised model property errors, or protocol processing errors. An [operation error](#operation-error) does not necessarily translate into a protocol error (it's up to the protocol emitter).
@@ -89,11 +93,27 @@ It specifies that the [operation](#operation) or model property will handle the The `errors` parameter is a list of models that represent the errors that will be handled by the [operation](#operation) or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. -#### [Operation errors](#operation-error) + `@raises` decorator -The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). For example, `getUser()` may return a `GenericError` in its [return type](#return-type), in addition to errors that may be associated with properties like `profilePictureUrl`. +## Interaction with Other TypeSpec Concepts + +### Operation Errors + +Earlier we defined [operation errors](#operation-error) as errors that are specified in the [return type](#return-type) of an [operation](#operation). -If an error type is specified in both the [operation](#operation)'s [return type](#return-type) and the `@raises` decorator, the [operation](#operation) will include the error (once) in the list of possible errors. +#### Operation errors + `@raises` decorator + +The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). For example, `getUser()` may have a `GenericError` in its [return type](#return-type), in addition to errors that may be associated with properties like `profilePictureUrl`.: + +```typespec +model User { + @raises(InvalidURLError) + @handles(PermissionDeniedError) + profilePictureUrl: string; +} +op getUser(id: string): User | GenericError; +``` + +If an error type is specified in both the [operation](#operation)'s [return type](#return-type) and the `@raises` decorator, the [protocol error](#operation) will include the error (once) in the list of possible errors. Semantically, the distinction between a `@raises` decorator and the [operation](#operation)'s [return type](#return-type) is in where the error is communicated. An error on a [return type](#return-type) indicates that the error is somehow exposed directly in that response. @@ -106,7 +126,7 @@ Essentially, an error in a [return type](#return-type) is opted out of any conte
-#### [Operation errors](#operation-error) + `@handles` decorator +#### Operation errors + `@handles` decorator It is possible, and valid, that an [operation](#operation) both `@handles` an error and also has a [return type](#return-type) that includes that error. In this case, the [operation](#operation) _will_ include the error in the list of possible errors for the operation. @@ -120,7 +140,7 @@ Semantically, this indicates that the [operation](#operation) will handle the `I This becomes important when considering error inheritance. -#### `@raises` + `@handles` decorator +### `@raises` + `@handles` decorator Model properties may have one or more error types defined in both their `@raises` decorator and the `@handles` decorator. In this case, the error is still considered possible at that property. Code emitters should treat `@raises` as taking precedence for code generation and documentation. @@ -132,10 +152,20 @@ model User { } ``` -#### Error inheritance + `@handles` decorator +is equivalent to: + +```typespec +model User { + @raises(InvalidURLError) + @handles(PermissionDeniedError) + profilePictureUrl: string; +} +``` + +### Error inheritance + `@handles` decorator Error handling is often handled generically. -When an error is specified in the `@handles` decorator, and there are additional errors that `extend` from it, those errors will also be considered as handled by the operation. +When an error is specified in the `@handles` decorator, and there are additional errors that `extend` from it, those errors will also be considered as handled. For example, if we were to specify that `getUser()` handles `GenericError`, it would also handle any errors that extend from `GenericError`, such as `NotFoundError` and `PermissionDeniedError`. @@ -155,15 +185,112 @@ model PermissionDeniedError extends GenericError {} op getUser(id: string): User | GenericError; ``` -Now, any errors thrown by any of the model properties used in the [operation](#operation) will not be added to the [operation](#operation)'s error output if they extend from `GenericError`. +This definition states that the protocol-specific behavior implied by the `@handles(GenericError)` decorator will also apply to `NotFoundError` and `PermissionDeniedError`. This inheritance does _not_ apply to the `@raises` decorator. If a property is decorated with `@raises(GenericError)`, it is not considered to be decorated with `@raises(NotFoundError)` or `@raises(PermissionDeniedError)`, even though those errors extend from `GenericError`. +In other words, given the following: + +```typespec +model Profile { + @raises(GenericError) + profilePictureUrl: string; +} + +model User { + @handles(NotFoundError, PermissionDeniedError) + profile: Profile; +} +``` + +would still consider `GenericError` to be a possible error at `User.profile` — it is not handled. + Conversely, if a property is decorated with `@raises(NotFoundError)`, it is not considered to be decorated with `@raises(GenericError)`. This approach aligns with the idea that error documentation should be explicit about which errors may occur at a given property, while allowing for more flexible handling in `@handles`. +### Compiler support for propagating errors to operations + +It will be a common case for a protocol to want to "propagate" the errors specified by `@raises` and `@handles` decorators with the errors specified in the [operation](#operation)'s [return type](#return-type). + +To make this easier, the TypeSpec compiler should include functionality to merge the error specification defined by `@raises` and `@handles` decorators into the [operation](#operation)'s [return type](#return-type). + +Looking at the following example: + +```typespec +model Profile { + @raises(InvalidURLError, PermissionDeniedError) + profilePictureUrl: string; +} + +model User { + @raises(NotFoundError) + @handles(PermissionDeniedError) + profile: Profile; +} + +@handles(NotFoundError, PrivateProfileError) +op getUser(id: string): User | GenericError | PrivateProfileError; +``` + +Some functionality in the TypeSpec compiler — let's call it `getOperationErrors()` — would a `getUser` operation type with the following signature: + +```typespec +op getUser(id: string): User | GenericError | InvalidURLError | PrivateProfileError; +``` + +Note the compiler has combined the return type with errors that were present in `@raises` decorators and not `@handles` decorators. +In this case, that means the return type consists of: +- `User`, as defined in `getUser()`'s return type +- `GenericError`, as defined in `getUser()`'s return type +- `InvalidURLError`, as defined in `Profile`'s `@raises` decorator and not in an applicable `@handles` decorator +- `PrivateProfileError`, as defined in `getUser()`'s return type. This follows [the precedence rule between `@raises` and `@handles`](#raises--handles-decorator) as if the error in the return type is an implicit `@raises` decorator. +- *not* `PermissionDeniedError`, as it is handled by `User.profile` +- *not* `NotFoundError`, as it is handled by `getUser()` + +
+ +## Use in request input + +The `@raises` and `@handles` decorators apply equally to input as they do to output. +Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. +The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output. + +
+ +### `@raises` for Input Validation Errors + +When applied to input properties, the `@raises` decorator specifies the errors that may occur during the validation or processing of client-provided data. +For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: + +```typespec +model CreateUserRequest { + @raises(InvalidEmailError, MissingFieldError) + email: string; + + @raises(InvalidPasswordError) + password: string; +} +``` + +These errors are generated by the server in response to invalid or incomplete input provided by the client. +This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. + +
+ +### `@handles` for Input-Level Error Handling + +The `@handles` decorator can be used to specify which input-related errors are handled by the [operation](#operation) itself, preventing them from being propagated to the client. +For example, an [operation](#operation) to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: + +```typespec +@handles(InvalidEmailError) +op createUser(request: CreateUserRequest): User | GenericError; +``` + +This behavior mirrors how `@handles` is used for output errors, allowing developers to control which errors are exposed via a [protocol error](#protocol-error) and which are handled internally. +
## Implementations and Use Cases @@ -324,6 +451,8 @@ The definition of `getUser()` has not changed, but it is now emitted as if the [ User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; ``` +To implement this, the OpenAPI emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). +
#### Using `@handles` decorator @@ -575,6 +704,8 @@ Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism fo The `@raises` decorator can be used to specify errors that may occur when accessing a property. These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. +To implement this, the Protobuf emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). + For example:
Click to collapse @@ -766,6 +897,8 @@ service UserService {
+To implement this, the Thrift emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). + #### Using `@handles` with Thrift The `@handles` decorator can be used to specify which exceptions are handled internally by an [operation](#operation) or property. In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. @@ -936,92 +1069,6 @@ Here, the server explicitly handles errors when resolving the `profile_picture_u
-## Use in request input - -The `@raises` and `@handles` decorators apply equally to input as they do to output. -Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. -The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output. - -
- -### `@raises` for Input Validation Errors - -When applied to input properties, the `@raises` decorator specifies the errors that may occur during the validation or processing of client-provided data. -For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: - -```typespec -model CreateUserRequest { - @raises(InvalidEmailError, MissingFieldError) - email: string; - - @raises(InvalidPasswordError) - password: string; -} -``` - -These errors are generated by the server in response to invalid or incomplete input provided by the client. -This is conceptually different from output errors, which are typically generated by the server's internal logic or data access operations. -However, the propagation of errors from model properties to the [operation](#operation)'s [return type](#return-type) works the same way for input as it does for output. - -
- -### `@handles` for Input-Level Error Handling - -The `@handles` decorator can be used to specify which input-related errors are handled by the [operation](#operation) itself, preventing them from being propagated to the client. -For example, an [operation](#operation) to create a user might handle `InvalidEmailError` internally while allowing other errors to propagate: - -```typespec -@handles(InvalidEmailError) -op createUser(request: CreateUserRequest): User | GenericError; -``` - -This behavior mirrors how `@handles` is used for output errors, allowing developers to control which errors are exposed to the client and which are handled internally. - -
- -### Error Propagation for Input - -Errors specified in `@raises` on input properties propagate to [operations](#operation) unless explicitly handled with `@handles`. -For example, the following [operation](#operation) automatically includes `InvalidEmailError`, `MissingFieldError`, and `InvalidPasswordError` in its error [return type](#return-type)s because they are specified in the `CreateUserRequest` model: - -```typespec -op createUser(request: CreateUserRequest): - | User - | InvalidEmailError - | MissingFieldError - | InvalidPasswordError - | GenericError; -``` - -This ensures consistency between input and output error modeling. -By default, errors propagate from input properties to operations, but [operations](#operation) can override this behavior with `@handles`. - -
- -### Protocol-Specific Behavior - -Input-related errors can also be tied to specific protocol behaviors. -For example, errors can be associated with HTTP status codes or GraphQL-specific behaviors. -The following example shows how to use the `@statusCode` decorator to specify that `InvalidEmailError` and `MissingFieldError` should result in HTTP 400 responses: - -```typespec -@error -model InvalidEmailError { - @statusCode _: 400; - message: string; -} - -@error -model MissingFieldError { - @statusCode _: 400; - message: string; -} -``` - -This is consistent with how protocol-specific metadata is applied to output errors, ensuring that input errors are handled appropriately in the context of the protocol being used. - -
- ## Alternatives Considered ### Mimic error handling in operations @@ -1040,11 +1087,6 @@ model NotFoundError { op getUser(id: string): User | NotFoundError; ``` -The `@raises` decorator is different from the [return type](#return-type) of [operations](#operation) in that it is used to document errors that may occur when accessing a property. - -This distinction is useful when a property itself may inherently produce errors, regardless of the [operation](#operation) in which it is used. -For example, accessing a property that requires a network fetch or a permission check may result in errors. - Using a union type, as [operations](#operation) do, does not allow for the same error semantic in model properties. Instead, such a type would be indicative of possible types for the property's _value_: @@ -1065,14 +1107,7 @@ model User { ``` This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. -Typically, this means that the property does not _have_ a value in that scenario and instead should be used to describe the appropriate error-returning semantic of a given protocol. - -## Summary - -The `@raises` and `@handles` decorators provide a unified framework for modeling and handling errors across both input and output scenarios. -While the mechanics of these decorators are identical for input and output, the use cases differ slightly. -Input errors are generated by the server in response to invalid or incomplete client-provided data, whereas output errors are typically generated by the server's internal logic or data access operations. -This distinction ensures that the proposal remains flexible and applicable to a wide range of error-handling scenarios. +Typically, this means that the property does not _have_ a value in that scenario and instead should be used to describe the appropriate [protocol error](#protocol-error).
@@ -1082,7 +1117,7 @@ This distinction ensures that the proposal remains flexible and applicable to a ### Optional: Adding Context Modifiers to `@error` -As an optional enhancement, we propose extending the `@error` decorator to include an argument for specifying context (visibility) modifiers. +As an optional enhancement, we propose extending the [`@error` decorator][error-decorator] to include an argument for specifying [context (visibility) modifiers][visibility-system]. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. @@ -1265,6 +1300,7 @@ As a result, the TypeSpec compiler will issue a warning. # +[typespec-operation]: https://typespec.io/docs/language-basics/operations/ [error-decorator]: https://typespec.io/docs/standard-library/built-in-decorators/#@error [operations]: https://typespec.io/docs/language-basics/operations/ [operations-return-type]: https://typespec.io/docs/language-basics/operations/#return-type From a1087709db21f4fe447e580a6c64dd3bc94a3229 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Fri, 6 Jun 2025 10:12:42 -0700 Subject: [PATCH 15/18] Add real-world use cases, goals and implementation considerations --- .../model-property-error-handling.md | 225 +++++++++++++++--- 1 file changed, 195 insertions(+), 30 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 9e7bde2a7ce..4aea7cb0aaa 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -11,9 +11,14 @@ The proposal also recommends that new and existing emitters support these decora ## Goals -1. Enable TypeSpec developers to document where errors may occur in model properties. -2. Provide emitters with information to update the set of [operation errors](#operation-error) based on the models in use and the error handling of the operation. -3. Allow code emitters to generate code that is aware of and can respond to these errors appropriately. +As we (Pinterest) are building out the [GraphQL emitter][graphql-emitter], we have identified a need to express complex error handling patterns that are common in GraphQL APIs and cannot be easily expressed with the existing TypeSpec error handling mechanisms. + +**Primary Goal:** Enable Pinterest's GraphQL emitter to express GraphQL's complex field-level error patterns (null propagation, errors-as-data, resolver-level handling). + +**Secondary Goals:** +1. Provide a standard way for other emitters to leverage field-level error information +2. Enable multi-protocol schemas to express error handling once, generate appropriately for each target +3. Support workflow orchestration and microservices patterns that require similar error handling
@@ -1069,61 +1074,221 @@ Here, the server explicitly handles errors when resolving the `profile_picture_u
-## Alternatives Considered +## Real-world Use Cases -### Mimic error handling in operations +**Note:** While Pinterest's immediate need is GraphQL, these patterns appear across multiple domains where field-level failures require different handling strategies. -TypeSpec [operations][operations] allow for specifying possible errors that the [operation](#operation) may produce via the [operation's return type][operations-return-type]. -The standard pattern is to use a union type that includes the models representing the errors, which have been decorated with the [`@error` decorator][error-decorator]. +This section is meant to demonstrate a few areas in real-world use where this proposal allows a new kind of error handling that is not currently possible. +Some may be more esoteric and/or speculative than others, but the goal is to explore a wide spectrum of use cases. -For example: +### Azure Logic Apps Workflow Orchestration + +Azure Logic Apps represents a compelling use case for field-level error handling specifications. +Logic Apps workflows consist of multiple actions that can fail independently, with subsequent actions configured to handle specific failure types through "run after" settings. + +Consider a workflow that retrieves user data and processes it through multiple services: ```typespec -@error -model NotFoundError { - message: string; -} +model UserProfileData { + @raises(UserNotFoundError, PermissionDeniedError) + basicInfo: UserInfo; + + @raises(ServiceUnavailableError, RateLimitExceededError) + socialMediaLinks: SocialLinks; + + @raises(NetworkTimeoutError) + profileImage: ImageData; +} + +@handles(UserNotFoundError, PermissionDeniedError) +op createDefaultProfile(userData: UserProfileData): UserProfile; + +@handles(ServiceUnavailableError) +op retryWithBackoff(userData: UserProfileData): UserProfile; +``` -op getUser(id: string): User | NotFoundError; +This TypeSpec definition directly maps to Logic Apps patterns: + +- **`@raises` decorators** specify which actions in the workflow can produce specific error types +- **`@handles` decorators** correspond to "run after" configurations that execute subsequent actions only for certain failure conditions +- **Error inheritance** mirrors how Logic Apps scopes can handle categories of related failures + +When generating Logic Apps workflow definitions from TypeSpec, an emitter could: + +1. **Generate appropriate "run after" configurations** based on `@handles` decorators +2. **Create error handling scopes** that catch specific error types from `@raises` specifications +3. **Implement retry policies** tailored to the documented error types +4. **Generate monitoring and alerting** based on the expected error scenarios + +This approach enables Logic Apps developers to model complex error handling scenarios in TypeSpec and generate robust, fault-tolerant workflows with proper error boundaries and recovery mechanisms. + +### Apollo Federation - Subgraph Error Boundaries + +In Apollo Federation, different fields can be resolved by different subgraphs, each with their own failure modes. +The gateway needs fine-grained control over error propagation and fallback strategies. + +```typespec +model Product { + @raises(ServiceUnavailableError) // Inventory service might be down + availableQuantity: int32; + + @raises(NotFoundError, PermissionDeniedError) // User service checks permissions + customerReviews: Review[]; + + @raises(NetworkTimeoutError) // External pricing API + dynamicPricing: Price; +} + +@handles(ServiceUnavailableError) // Gateway handles inventory failures gracefully +op getProductWithFallbacks(id: string): Product; ``` -Using a union type, as [operations](#operation) do, does not allow for the same error semantic in model properties. -Instead, such a type would be indicative of possible types for the property's _value_: +When the Apollo Gateway encounters a `ServiceUnavailableError` from the inventory subgraph, the `@handles` decorator allows it to return partial product data rather than failing the entire query. +This enables sophisticated error boundary patterns in federated GraphQL architectures. + +### Netflix-style Circuit Breaker Patterns + +Microservices architectures use circuit breakers for individual service calls, where different fields require different fallback strategies based on business criticality. ```typespec -model User { - profilePictureUrl: string | NotFoundError | PermissionDeniedError; -} +model UserDashboard { + @raises(RecommendationServiceError) // Can fallback to cached recommendations + personalizedContent: Content[]; + + @raises(BillingServiceError) // Critical - must show billing errors + accountStatus: AccountStatus; + + @raises(WatchHistoryError) // Can fallback to empty state + recentlyWatched: Video[]; +} + +@handles(RecommendationServiceError, WatchHistoryError) // Handle non-critical failures +op getDashboardWithFallbacks(userId: string): UserDashboard; ``` -The above TypeSpec implies that the property could be populated with either a string or one of two model type. -The fact that `NotFoundError` and `PermissionDeniedError` use the `@error` decorator is irrelevant. +This pattern allows critical errors (billing issues) to propagate while gracefully handling non-critical failures (recommendations, watch history) through fallbacks or cached data. + +### E-commerce Partial Product Data + +E-commerce platforms need to handle partial product availability where inventory, pricing, and content management systems can fail independently. ```typespec -model User { - @raises(NotFoundError, PermissionDeniedError) - profilePictureUrl: string; -} +model ProductPage { + @raises(InventoryServiceError) // Inventory might be temporarily unavailable + stockStatus: StockInfo; + + @raises(PricingServiceError) // Pricing service might be updating + currentPrice: Price; + + @raises(ContentServiceError) // CMS might be down + productDescription: string; +} + +@handles(InventoryServiceError) // Show "availability unknown" instead of failing +op getProductPageWithDefaults(productId: string): ProductPage; ``` -This TypeSpec, by contrast, indicates that the `profilePictureUrl` property's value is always a string, but that accessing it may produce either a `NotFoundError` or a `PermissionDeniedError`. -Typically, this means that the property does not _have_ a value in that scenario and instead should be used to describe the appropriate [protocol error](#protocol-error). +This enables platforms like Shopify to show "availability unknown" or cached pricing when specific services are down, rather than showing broken product pages. + +### Financial Trading Platforms - Data Quality Control + +Financial systems require different error handling strategies based on data criticality and regulatory requirements. + +```typespec +model MarketData { + @raises(StaleDataError, ConnectivityError) // Real-time feed issues + livePrice: decimal; + + @raises(DataQualityError) // Historical data corruption + volumeHistory: VolumeData[]; + + @raises(ExchangeUnavailableError) // Exchange-specific issues + orderBook: OrderBookData; +} + +@handles(StaleDataError) // Use last-known-good price for trading decisions +op getTradingData(symbol: string): MarketData; +``` + +Trading systems can use the last-known-good price when live feeds are stale, while still surfacing data quality issues that might affect compliance or trading decisions. + +### Content Management Systems - Progressive Enhancement + +CMS platforms where page components can fail independently but the page should still render with graceful degradation. + +```typespec +model WebPage { + @raises(CDNError) // Images might not load + heroImage: ImageUrl; + + @raises(DatabaseError) // Content might be temporarily unavailable + mainContent: RichText; + + @raises(APIRateLimitError) // Social feeds might be rate-limited + socialFeed: SocialPost[]; +} + +@handles(CDNError, APIRateLimitError) // Show placeholders for non-critical content +op renderPageWithDefaults(pageId: string): WebPage; +``` + +This enables progressive enhancement patterns where critical content (main text) failures propagate as errors, while non-critical elements (images, social feeds) show placeholders or cached content. + +### IoT Data Aggregation - Sensor Reliability + +IoT platforms where individual sensors can fail but the system needs to provide meaningful partial data for monitoring and control systems. + +```typespec +model EnvironmentReading { + @raises(SensorOfflineError, CalibrationError) // Temperature sensor issues + temperature: float; + + @raises(NetworkTimeoutError) // Connectivity problems + humidity: float; + + @raises(BatteryLowError) // Power management issues + airQuality: AirQualityReading; +} + +@handles(SensorOfflineError, BatteryLowError) // Use estimated values for failed sensors +op getEnvironmentData(stationId: string): EnvironmentReading; +``` + +Environmental monitoring systems can use interpolated values from nearby sensors when individual sensors fail, while still alerting operators to calibration issues that require intervention. + +## Phased Implementation Approach + +We suggest implementation of this proposal follow a two-phase approach to allow for community feedback and refinement before finalizing the design. + +**Phase 1 (Experimental):** +- Implement `@raises` and `@handles` decorators +- Mark as `@experimental` in TypeSpec core +- Ship Pinterest GraphQL emitter as reference implementation +- Add support in the OpenAPI emitter (needed by Pinterest) +- Gather community feedback + +**Phase 2 (Stable):** +- Refine based on real-world usage +- Add context modifiers if validated by community needs +- Remove experimental status +- Adopt in other emitters -
## Additional Considerations -### Optional: Adding Context Modifiers to `@error` +### Future Enhancement: Context Modifiers + +**Note:** This is explicitly NOT part of the initial proposal. +We propose implementing the core `@raises`/`@handles` functionality first, then evaluating whether context modifiers are needed based on real usage patterns. As an optional enhancement, we propose extending the [`@error` decorator][error-decorator] to include an argument for specifying [context (visibility) modifiers][visibility-system]. This would allow developers to explicitly indicate the contexts in which an error applies, such as input validation, output handling, or both. This enhancement would provide additional clarity and flexibility when modeling errors. -**This does not change the core mechanics of the `@raises` and `@handles` decorators, and the proposal for those decorators remains unchanged whether or not this enhancement is adopted.** -### Proposed Definition +#### Proposed Definition The `@error` decorator would accept an optional argument specifying one or more visibility enums. From 5d10053828a110aa39cc7432670c56f947f71940 Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Mon, 9 Jun 2025 15:39:12 -0700 Subject: [PATCH 16/18] additional polish --- .../model-property-error-handling.md | 395 ++++++++++-------- 1 file changed, 221 insertions(+), 174 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index 4aea7cb0aaa..e54e4649f0e 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -17,8 +17,8 @@ As we (Pinterest) are building out the [GraphQL emitter][graphql-emitter], we ha **Secondary Goals:** 1. Provide a standard way for other emitters to leverage field-level error information -2. Enable multi-protocol schemas to express error handling once, generate appropriately for each target -3. Support workflow orchestration and microservices patterns that require similar error handling +2. Enable multi-protocol schemas to express error handling once and generate appropriately for each target +3. Make error handling documentation more consistent and accurate by computing error information that must otherwise be specified manually
@@ -98,16 +98,23 @@ It specifies that the [operation](#operation) or model property will handle the The `errors` parameter is a list of models that represent the errors that will be handled by the [operation](#operation) or model property. Each model must be decorated with the [`@error` decorator][error-decorator]. +
## Interaction with Other TypeSpec Concepts +This section will discuss how to integrate the new decorators with existing TypeSpec concepts. + ### Operation Errors Earlier we defined [operation errors](#operation-error) as errors that are specified in the [return type](#return-type) of an [operation](#operation). +Following, we'll discuss how operations errors interact with the `@raises` and `@handles` decorators. + #### Operation errors + `@raises` decorator -The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). For example, `getUser()` may have a `GenericError` in its [return type](#return-type), in addition to errors that may be associated with properties like `profilePictureUrl`.: +The `@raises` decorator can be used alongside an [operation](#operation)'s [return type](#return-type). +For example, `getUser()` may have a `GenericError` in its [return type](#return-type), +in addition to errors that may be associated with properties like `profilePictureUrl`.: ```typespec model User { @@ -118,18 +125,18 @@ model User { op getUser(id: string): User | GenericError; ``` -If an error type is specified in both the [operation](#operation)'s [return type](#return-type) and the `@raises` decorator, the [protocol error](#operation) will include the error (once) in the list of possible errors. +If an error type is specified in both the [operation](#operation)'s [return type](#return-type) and the `@raises` decorator, +the [protocol error](#operation) should include the error (once) in the list of possible errors. Semantically, the distinction between a `@raises` decorator and the [operation](#operation)'s [return type](#return-type) is in where the error is communicated. -An error on a [return type](#return-type) indicates that the error is somehow exposed directly in that response. -An error specified with `@raises`, on the other hand, may appear in a different location depending on if or where the error is specified in a `@handles` decorator. +An error on a [return type](#return-type) is an explicit indication that the error is somehow exposed directly in that response. +An error specified with `@raises`, on the other hand, may appear in a different location depending on if or where the error is specified in a `@handles` decorator — or not at all, depending on the protocol. -For instance, a bulk [operation](#operation) of some kind that includes the results of several sub-[operations](#operation) could communicate errors in a few different ways. -One way would be for each of the [operations](#operation) in the bulk set to provide its error value as its specific [return type](#return-type) — as indicated by an error present in the [return type](#return-type).∆ -Another might be for the bulk [operation](#operation) to aggregate all the errors that occurred in the sub-[operations](#operation) and communicate them somewhere in its own response, which could be accomplished by the sub-[operations](#operation) using the `@raises` decorator and the bulk [operation](#operation) using the `@handles` decorator. -Essentially, an error in a [return type](#return-type) is opted out of any contextual handling, while an error in a `@raises` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers). +For instance, a bulk [operation](#operation) of some kind that includes the results of several sub-operations could communicate errors in a few different ways. +One way would be for each of the operation in the bulk set to provide its error value as its specific [return type](#return-type) — as indicated by an error present in the [return type](#return-type). +Another might be for the bulk operation to aggregate all the errors that occurred in the sub-operations and communicate them somewhere in its own response, which could be accomplished by the sub-operations using the `@raises` decorator and the bulk operation using the `@handles` decorator. -
+Essentially, an error in a [return type](#return-type) is opted out of any contextual handling, while an error in a `@raises` decorator follows the rules specified by other operations, properties, and/or [contextual modifiers](#context-modifiers). #### Operation errors + `@handles` decorator @@ -145,7 +152,9 @@ Semantically, this indicates that the [operation](#operation) will handle the `I This becomes important when considering error inheritance. -### `@raises` + `@handles` decorator +
+ +### Interaction between`@raises` and `@handles` decorators Model properties may have one or more error types defined in both their `@raises` decorator and the `@handles` decorator. In this case, the error is still considered possible at that property. Code emitters should treat `@raises` as taking precedence for code generation and documentation. @@ -167,12 +176,24 @@ model User { } ``` -### Error inheritance + `@handles` decorator +
+ +### Error inheritance + +Most languages have a way to specify that an error type inherits from another error type. + +For the purposes of discussion, let's imagine we have a base error type `GenericError` and two errors that extend it: `NotFoundError` and `PermissionDeniedError`. -Error handling is often handled generically. -When an error is specified in the `@handles` decorator, and there are additional errors that `extend` from it, those errors will also be considered as handled. +#### Error inheritance +`@handles` decorator -For example, if we were to specify that `getUser()` handles `GenericError`, it would also handle any errors that extend from `GenericError`, such as `NotFoundError` and `PermissionDeniedError`. +Error handling is often performed generically based on a base error type, +allowing the developer to handle errors that were not known at the time of writing the code. + +Therefore when an error is specified in the `@handles` decorator, and there are additional errors that `extend` from it, +those errors will also be considered as handled. + +For example, if we were to specify that `getUser()` handles `GenericError`, +we are also specifying that it will handle `NotFoundError` and `PermissionDeniedError` as well as any other error that extends `GenericError`. ```typespec @error @@ -187,13 +208,18 @@ model NotFoundError extends GenericError {} model PermissionDeniedError extends GenericError {} @handles(GenericError) -op getUser(id: string): User | GenericError; +op getUser(id: string): User; ``` This definition states that the protocol-specific behavior implied by the `@handles(GenericError)` decorator will also apply to `NotFoundError` and `PermissionDeniedError`. -This inheritance does _not_ apply to the `@raises` decorator. -If a property is decorated with `@raises(GenericError)`, it is not considered to be decorated with `@raises(NotFoundError)` or `@raises(PermissionDeniedError)`, even though those errors extend from `GenericError`. +#### Error inheritance +`@raises` decorator + +The inheritance described above for `@handles` does _not_ apply to the `@raises` decorator. + +If a property is decorated with `@raises(GenericError)`, +it is not implying anything about whether the property can raise `NotFoundError` or `PermissionDeniedError`, +even though those errors extend from `GenericError`. In other words, given the following: @@ -209,50 +235,31 @@ model User { } ``` -would still consider `GenericError` to be a possible error at `User.profile` — it is not handled. +We would still consider `GenericError` to be a possible error at `User.profile` — it is not handled by the `@handles` decorator. Conversely, if a property is decorated with `@raises(NotFoundError)`, it is not considered to be decorated with `@raises(GenericError)`. -This approach aligns with the idea that error documentation should be explicit about which errors may occur at a given property, while allowing for more flexible handling in `@handles`. - -### Compiler support for propagating errors to operations - -It will be a common case for a protocol to want to "propagate" the errors specified by `@raises` and `@handles` decorators with the errors specified in the [operation](#operation)'s [return type](#return-type). - -To make this easier, the TypeSpec compiler should include functionality to merge the error specification defined by `@raises` and `@handles` decorators into the [operation](#operation)'s [return type](#return-type). - -Looking at the following example: +It follows that a `@raises` decorator can contain multiple errors that form an inheritance hierarchy — i.e. this is not redundant. ```typespec model Profile { - @raises(InvalidURLError, PermissionDeniedError) + @raises(NotFoundError, PermissionDeniedError, GenericError) profilePictureUrl: string; } +``` +When combined with the `@handles` decorator, any error that is not covered by its own type or a supertype is considered unhandled. + +```typespec model User { - @raises(NotFoundError) - @handles(PermissionDeniedError) + @handles(NotFoundError, PermissionDeniedError) profile: Profile; } - -@handles(NotFoundError, PrivateProfileError) -op getUser(id: string): User | GenericError | PrivateProfileError; ``` -Some functionality in the TypeSpec compiler — let's call it `getOperationErrors()` — would a `getUser` operation type with the following signature: - -```typespec -op getUser(id: string): User | GenericError | InvalidURLError | PrivateProfileError; -``` +The above example suggests that `User.profile` will not raise `NotFoundError` or `PermissionDeniedError`, but it may raise any other type of `GenericError`. -Note the compiler has combined the return type with errors that were present in `@raises` decorators and not `@handles` decorators. -In this case, that means the return type consists of: -- `User`, as defined in `getUser()`'s return type -- `GenericError`, as defined in `getUser()`'s return type -- `InvalidURLError`, as defined in `Profile`'s `@raises` decorator and not in an applicable `@handles` decorator -- `PrivateProfileError`, as defined in `getUser()`'s return type. This follows [the precedence rule between `@raises` and `@handles`](#raises--handles-decorator) as if the error in the return type is an implicit `@raises` decorator. -- *not* `PermissionDeniedError`, as it is handled by `User.profile` -- *not* `NotFoundError`, as it is handled by `getUser()` +This approach aligns with the idea that error documentation should be explicit about which errors may occur at a given property, while allowing for more flexible handling in `@handles`.
@@ -260,13 +267,13 @@ In this case, that means the return type consists of: The `@raises` and `@handles` decorators apply equally to input as they do to output. Just as these decorators allow developers to model and handle errors that may occur when accessing properties in a server's response, they can also be used to model and handle errors that arise when processing client-provided input. -The mechanics of how these decorators are applied and how they affect the emitted output remain consistent between input and output. +The mechanics of how these decorators are applied and how they affect the emitted document(s) remain consistent between input and output.
### `@raises` for Input Validation Errors -When applied to input properties, the `@raises` decorator specifies the errors that may occur during the validation or processing of client-provided data. +When applied to model properties used on input, the `@raises` decorator specifies the errors that may occur during the validation or processing of client-provided data. For example, an input model for creating a user might specify that the `email` field can produce `InvalidEmailError` or `MissingFieldError`, while the `password` field can produce `InvalidPasswordError`: ```typespec @@ -296,23 +303,35 @@ op createUser(request: CreateUserRequest): User | GenericError; This behavior mirrors how `@handles` is used for output errors, allowing developers to control which errors are exposed via a [protocol error](#protocol-error) and which are handled internally. +### A note on context modifiers + +Through the [visibility system][visibility-system], we know that a single model property may be both an input and an output property. + +It may indeed be the case that some errors are only relevant to the property when it is used as an input, while others are only relevant when it is used as an output, while still others may be relevant in both contexts. + +The suggestion for the developer is to err on the side of caution and specify both input and output errors in the `@raises` decorator. +This may cause some unnecessary error handling in clients, but this is preferable to unexpected errors. + +For a more nuanced approach, we can consider applying [context modifiers](#context-modifiers) to errors. +
## Implementations and Use Cases -Below we list some proposed implementations in various emitter targets. These are meant to be illustrative of the effects of the `@raises` and `@handles` decorators, and are not proposing any of the specific syntax or implementation shown below. +Below we list some proposed implementations in various emitter targets. +These are meant to be illustrative of the effects of the `@raises` and `@handles` decorators, +and are not proposing any of the specific syntax or implementation shown below. ### HTTP/REST/OpenAPI -In a typical HTTP/REST API where [operations](#operation) are represented by endpoints, the `@raises` decorator can provide more accurate [return type](#return-type)s for [operations](#operation) that contain properties that may fail. +In a typical HTTP/REST API where [operations](#operation) are represented by endpoints, +the `@raises` decorator can provide more accurate [return type](#return-type)s for [operations](#operation) that contain properties that may fail. -In a larger API, it may be quite difficult to track all of the errors that can occur within an [operation](#operation) when the errors can be generated by many different layers of an API stack. +In a larger API, it may be quite difficult to track all the errors that can occur within an [operation](#operation) when the errors can be generated by many different layers of an API stack. The `@raises` decorator helps give the developer a more complete view of the errors that an [operation](#operation) can produce. Let's say we have this definition of models: -
Click to collapse - ```typespec import "@typespec/http"; using Http; @@ -322,29 +341,12 @@ model GenericError { message: string; } -@error -model NotFoundError extends GenericError { - @statusCode _: 404; -} - -@error -model PermissionDeniedError extends GenericError { - @statusCode _: 403; -} - -@error -model InvalidURLError extends GenericError { - @statusCode _: 500; -} - model User { @key id: string; profilePictureUrl: string; } ``` -
- Now we define an [operation](#operation) that uses the `User` model: ```typespec @@ -392,6 +394,21 @@ paths: With the `@raises` decorator, we can specify that the `profilePictureUrl` property may produce errors when accessed: ```typespec +@error +model NotFoundError extends GenericError { + @statusCode _: 404; +} + +@error +model PermissionDeniedError extends GenericError { + @statusCode _: 403; +} + +@error +model InvalidURLError extends GenericError { + @statusCode _: 500; +} + model User { @key id: string; @@ -400,7 +417,8 @@ model User { } ``` -Since the `User` model is used in the `getUser()` operation, the generated OpenAPI will now include the possible errors that can occur when accessing the `profilePictureUrl` property: +Since the `User` model is used in the `getUser()` operation, +the operation schema in the generated OpenAPI will now include the possible errors that can occur when accessing the `profilePictureUrl` property:
Click to collapse @@ -450,13 +468,13 @@ paths:
-The definition of `getUser()` has not changed, but it is now emitted as if the [return type](#return-type) was +The definition of `getUser()` has not changed, but it is now emitted _as if_ the [return type](#return-type) was ```typespec User | NotFoundError | PermissionDeniedError | InvalidURLError | GenericError; ``` -To implement this, the OpenAPI emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). +To implement this, the OpenAPI emitter could take advantage of [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type).
@@ -472,7 +490,8 @@ We can use the `@handles` decorator to specify that this [operation](#operation) op getUser(@path id: string): User | GenericError; ``` -Now, despite the presence of a `User.profilePictureUrl` property that may produce an `InvalidURLError`, the OpenAPI will not include it in the list of possible errors for the `getUser()` operation: +Now, despite the presence of a `User.profilePictureUrl` property that may produce an `InvalidURLError`, +the OpenAPI will not include it in the list of possible errors for the `getUser()` operation:
Click to collapse @@ -523,21 +542,57 @@ Any property that is decorated with `@raises(InvalidURLError)` and is used in th ### GraphQL -In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]. -The `@raises` decorator can be used to document which errors may occur when resolving a specific field. -For example, a field decorated with `@raises` could generate GraphQL schema documentation indicating the possible errors. +In GraphQL, errors are typically propagated through the [`errors` key in the response][graphql-errors]: + +
Click to collapse + +```json +{ + "data": { + "user": null + }, + "errors": [ + { + "message": "User not found", + "locations": [{ "line": 2, "column": 3 }], + "path": ["user"], + "extensions": { + "code": "NOT_FOUND", + "exception": { "stacktrace": [...] } + } + } + ] +} +``` + +
+ +Since an error can occur within any GraphQL resolver, +we need a way to associate errors with anything that can have a resolver — which is any operation or model property. -Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], where errors are included in the possible value of a field using union types. +To represent this complexity with current TypeSpec concepts, we would perhaps need to modify the value type of the field to be a union type that includes the error type. +However, this would change the shape of the TypeSpec API description to accommodate a specific protocol's error handling pattern. +Other protocols like OpenAPI would now inaccurately document that the field's value can be an error type, which is unlikely to be true in practice. + +Using the `@raises` decorator on model properties avoids this and enhances the ability of the TypeSpec document to emit multiple protocols. + +#### Propagation and "Errors as Data" + +Some GraphQL schemas use the ["errors as data" pattern][errors-as-data], +where errors are included in the possible value of a field using union types. In this case, the `@raises` decorator can be used to specify which errors must be included in that union type. -The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. +The forthcoming [GraphQL emitter][graphql-emitter] will include additional decorators that can be applied to error models, +similar to `@typespec/http`'s [`@statusCode` decorator][statuscode-decorator]. These decorators can be used to customize how errors in a `@raises` decorator are emitted in the GraphQL schema. For example, a `@propagate` decorator could be used to indicate that an error type, if produced, should be propagated to parent fields. -In GraphQL, this is accomplished by making a field type non-nullable, which means that if a value cannot be produced for that field (due to an error), the error will be bubble up through parent fields, stopping at the first field which is nullable. +In GraphQL, this is accomplished by making a field type non-nullable — meaning that if a value cannot be produced for that field (due to an error), +the error will be bubble up through parent fields, stopping at the first field which is nullable. A `@asData` decorator could be used to indicate that an error type should be included in the ["errors as data" pattern][errors-as-data]. -This allows a GraphQL schema to opt-in to using this pattern for specific errors, while still allowing other errors (e.g. unexpected server errors) to be propagated normally. This avoids changing the shape of the TypeSpec API description to serve the needs of a specific protocol. +This allows a GraphQL schema to opt-in to using this pattern for specific errors, +while still allowing other errors (e.g. unexpected server errors) to be propagated normally. The `@handles` decorator can also be used in GraphQL to specify that a field resolver will handle certain types of errors. Specifying an error in the `@handles` decorator will: @@ -669,6 +724,7 @@ union UserProfilePictureUrlResponse = | String | NotFoundError # NotFoundError is `@asData`, so it's added to the union | ClientError # PermissionDeniedError does not use `@asData`, but it extends from ClientError which does + type User { """ A log of the user's activity @@ -703,13 +759,16 @@ type ActivityEntry { ### Protocol Buffers (Protobuf) -Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. While Protobuf itself does not have a built-in concept of errors, the `@raises` and `@handles` decorators can be used to model and document errors in TypeSpec, which can then be translated into Protobuf-compatible patterns. Different patterns for communicating errors in Protobuf can be expressed using additional TypeSpec decorators. +Protocol Buffers (Protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. +While Protobuf itself does not have a built-in concept of errors, the `@raises` and `@handles` decorators can be used to model and document errors in TypeSpec, which can then be translated into Protobuf-compatible patterns. +Different patterns for communicating errors in Protobuf can be expressed using additional TypeSpec decorators. #### Using `@raises` with Protobuf -The `@raises` decorator can be used to specify errors that may occur when accessing a property. These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. +The `@raises` decorator can be used to specify errors that may occur when accessing a property. +These errors can be represented in Protobuf by defining separate message types for each error and including them in a `oneof` field in the response message. -To implement this, the Protobuf emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). +To implement this, the Protobuf emitter could take advantage of [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). For example: @@ -733,8 +792,6 @@ model User { profilePictureUrl: string; } -@route("/user/{id}") -@get op getUser(@path id: string): User; ``` @@ -799,8 +856,6 @@ model User { @raises(NotFoundError) profilePictureUrl: string; } -@route("/user/{id}") -@get op getUser(@path id: string): User | Error; ``` @@ -842,7 +897,8 @@ message GetUserRequest { ### Apache Thrift -Apache Thrift supports defining exceptions as part of its IDL (Interface Definition Language), which makes it well-suited for modeling error using the `@raises` and `@handles` decorators. +Apache Thrift supports defining exceptions as part of its IDL (Interface Definition Language), +which makes it well-suited for modeling error using the `@raises` and `@handles` decorators. #### Using `@raises` with Thrift @@ -868,8 +924,6 @@ model User { profilePictureUrl: string; } -@route("/user/{id}") -@get op getUser(@path id: string): User; ``` @@ -902,17 +956,16 @@ service UserService {
-To implement this, the Thrift emitter could take advantage of the [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). +To implement this, the Thrift emitter could take advantage of [TypeSpec compiler support](#compiler-support-for-propagating-errors-to-operations) to propagate errors from model properties to the [operation](#operation)'s [return type](#return-type). #### Using `@handles` with Thrift -The `@handles` decorator can be used to specify which exceptions are handled internally by an [operation](#operation) or property. In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. +The `@handles` decorator can be used to specify which exceptions are handled internally by an [operation](#operation) or property. +In Thrift, this can be reflected by omitting the handled exceptions from the `throws` clause of the service method. For example: ```typespec -@route("/user/{id}") -@get @handles(PermissionDeniedError) op getUser(@path id: string): User; ``` @@ -932,7 +985,8 @@ service UserService { ### Client libraries Client libraries should leverage language-specific constructs to represent fields or [operations](#operation) that may produce errors. -For example, in languages with an error monad or result monad, such as Kotlin or Swift, these constructs should be used to represent fields decorated with `@raises` or [operations](#operation) decorated with `@handles`. +For example, in languages with an error monad or result monad, such as Kotlin or Swift, +these constructs should be used to represent fields decorated with `@raises` or [operations](#operation) decorated with `@handles`. #### Example: Kotlin @@ -1013,7 +1067,8 @@ This approach ensures that clients handle errors in a type-safe and idiomatic wa ### Server libraries Server libraries should generate code that includes appropriate error handling stubs. -For example, in languages with an error monad or result monad, these constructs should be used to represent fields or [operations](#operation) that may produce errors. +For example, in languages with an error monad or result monad, +these constructs should be used to represent fields or [operations](#operation) that may produce errors. This allows server implementations to handle errors explicitly and propagate them as needed. #### Example: Scala @@ -1076,7 +1131,8 @@ Here, the server explicitly handles errors when resolving the `profile_picture_u ## Real-world Use Cases -**Note:** While Pinterest's immediate need is GraphQL, these patterns appear across multiple domains where field-level failures require different handling strategies. +**Note:** While Pinterest's immediate need is GraphQL, +patterns that require similar nuance in error handling appear across multiple domains. This section is meant to demonstrate a few areas in real-world use where this proposal allows a new kind of error handling that is not currently possible. Some may be more esoteric and/or speculative than others, but the goal is to explore a wide spectrum of use cases. @@ -1084,7 +1140,8 @@ Some may be more esoteric and/or speculative than others, but the goal is to exp ### Azure Logic Apps Workflow Orchestration Azure Logic Apps represents a compelling use case for field-level error handling specifications. -Logic Apps workflows consist of multiple actions that can fail independently, with subsequent actions configured to handle specific failure types through "run after" settings. +Logic Apps workflows consist of multiple actions that can fail independently, +with subsequent actions configured to handle specific failure types through ["run after"][run-after] behavior Consider a workflow that retrieves user data and processes it through multiple services: @@ -1122,35 +1179,19 @@ When generating Logic Apps workflow definitions from TypeSpec, an emitter could: This approach enables Logic Apps developers to model complex error handling scenarios in TypeSpec and generate robust, fault-tolerant workflows with proper error boundaries and recovery mechanisms. -### Apollo Federation - Subgraph Error Boundaries +### Netflix-style Circuit Breaker Patterns -In Apollo Federation, different fields can be resolved by different subgraphs, each with their own failure modes. -The gateway needs fine-grained control over error propagation and fallback strategies. +Microservices architectures use circuit breakers for individual service calls, +where different fields require different fallback strategies based on business criticality. ```typespec -model Product { - @raises(ServiceUnavailableError) // Inventory service might be down - availableQuantity: int32; - - @raises(NotFoundError, PermissionDeniedError) // User service checks permissions - customerReviews: Review[]; - - @raises(NetworkTimeoutError) // External pricing API - dynamicPricing: Price; -} +@error model CriticalError {} +@error model NonCriticalError {} -@handles(ServiceUnavailableError) // Gateway handles inventory failures gracefully -op getProductWithFallbacks(id: string): Product; -``` +@error model RecommendationServiceError extends NonCriticalError {} +@error model BillingServiceError extends CriticalError {} +@error model WatchHistoryError extends NonCriticalError {} -When the Apollo Gateway encounters a `ServiceUnavailableError` from the inventory subgraph, the `@handles` decorator allows it to return partial product data rather than failing the entire query. -This enables sophisticated error boundary patterns in federated GraphQL architectures. - -### Netflix-style Circuit Breaker Patterns - -Microservices architectures use circuit breakers for individual service calls, where different fields require different fallback strategies based on business criticality. - -```typespec model UserDashboard { @raises(RecommendationServiceError) // Can fallback to cached recommendations personalizedContent: Content[]; @@ -1162,7 +1203,7 @@ model UserDashboard { recentlyWatched: Video[]; } -@handles(RecommendationServiceError, WatchHistoryError) // Handle non-critical failures +@handles(NonCriticalError) // Handle non-critical failures op getDashboardWithFallbacks(userId: string): UserDashboard; ``` @@ -1188,29 +1229,8 @@ model ProductPage { op getProductPageWithDefaults(productId: string): ProductPage; ``` -This enables platforms like Shopify to show "availability unknown" or cached pricing when specific services are down, rather than showing broken product pages. - -### Financial Trading Platforms - Data Quality Control - -Financial systems require different error handling strategies based on data criticality and regulatory requirements. - -```typespec -model MarketData { - @raises(StaleDataError, ConnectivityError) // Real-time feed issues - livePrice: decimal; - - @raises(DataQualityError) // Historical data corruption - volumeHistory: VolumeData[]; - - @raises(ExchangeUnavailableError) // Exchange-specific issues - orderBook: OrderBookData; -} - -@handles(StaleDataError) // Use last-known-good price for trading decisions -op getTradingData(symbol: string): MarketData; -``` - -Trading systems can use the last-known-good price when live feeds are stale, while still surfacing data quality issues that might affect compliance or trading decisions. +This enables platforms like Shopify to show "availability unknown" or cached pricing when specific services are down, +rather than showing broken product pages. ### Content Management Systems - Progressive Enhancement @@ -1234,27 +1254,7 @@ op renderPageWithDefaults(pageId: string): WebPage; This enables progressive enhancement patterns where critical content (main text) failures propagate as errors, while non-critical elements (images, social feeds) show placeholders or cached content. -### IoT Data Aggregation - Sensor Reliability - -IoT platforms where individual sensors can fail but the system needs to provide meaningful partial data for monitoring and control systems. - -```typespec -model EnvironmentReading { - @raises(SensorOfflineError, CalibrationError) // Temperature sensor issues - temperature: float; - - @raises(NetworkTimeoutError) // Connectivity problems - humidity: float; - - @raises(BatteryLowError) // Power management issues - airQuality: AirQualityReading; -} - -@handles(SensorOfflineError, BatteryLowError) // Use estimated values for failed sensors -op getEnvironmentData(stationId: string): EnvironmentReading; -``` - -Environmental monitoring systems can use interpolated values from nearby sensors when individual sensors fail, while still alerting operators to calibration issues that require intervention. +
## Phased Implementation Approach @@ -1273,14 +1273,59 @@ We suggest implementation of this proposal follow a two-phase approach to allow - Remove experimental status - Adopt in other emitters +
## Additional Considerations +The following should be considered as future enhancements to enhance interaction with the `@raises` and `@handles` decorators. + +### TypeSpec Compiler support for propagating errors to operations + +It will be a common case for a protocol to want to "propagate" the errors specified by `@raises` and `@handles` decorators with the errors specified in the [operation](#operation)'s [return type](#return-type). + +To make this easier, the TypeSpec compiler may include functionality to merge the error specification defined by `@raises` and `@handles` decorators into the [operation](#operation)'s [return type](#return-type). + +Looking at the following example: + +```typespec +model Profile { + @raises(InvalidURLError, PermissionDeniedError) + profilePictureUrl: string; +} + +model User { + @raises(NotFoundError) + @handles(PermissionDeniedError) + profile: Profile; +} + +@handles(NotFoundError, PrivateProfileError) +op getUser(id: string): User | GenericError | PrivateProfileError; +``` + +Some functionality in the TypeSpec compiler — let's call it `getOperationErrors()` — would a `getUser` operation type with the following signature: + +```typespec +op getUser(id: string): User | GenericError | InvalidURLError | PrivateProfileError; +``` + +Note the compiler has combined the return type with errors that were present in `@raises` decorators and not `@handles` decorators. +In this case, that means the return type consists of: +- `User`, as defined in `getUser()`'s return type +- `GenericError`, as defined in `getUser()`'s return type +- `InvalidURLError`, as defined in `Profile`'s `@raises` decorator and not in an applicable `@handles` decorator +- `PrivateProfileError`, as defined in `getUser()`'s return type. This follows [the precedence rule between `@raises` and `@handles`](#raises--handles-decorator) as if the error in the return type is an implicit `@raises` decorator. +- *not* `PermissionDeniedError`, as it is handled by `User.profile` +- *not* `NotFoundError`, as it is handled by `getUser()` + +
+ ### Future Enhancement: Context Modifiers **Note:** This is explicitly NOT part of the initial proposal. +Adding context modifiers to errors introduces additional complexity, similar to the [visibility system][visibility-system]. We propose implementing the core `@raises`/`@handles` functionality first, then evaluating whether context modifiers are needed based on real usage patterns. As an optional enhancement, we propose extending the [`@error` decorator][error-decorator] to include an argument for specifying [context (visibility) modifiers][visibility-system]. @@ -1353,11 +1398,11 @@ model User { email: string; } -op getUser(id: string): User | UserNotFound; // will not include InvalidEmailError, does return email field +op getUser(id: string): User | UserNotFound; // returns email field in response, will not raise InvalidEmailError -op createUser(...User): User; // will include InvalidEmailError, does return email field +op createUser(...User): User; // returns email field in response, can raise InvalidEmailError -op deleteUser(id: string): User; // does not include InvalidEmailError, does not return email field +op deleteUser(id: string): User; // does not return email field in response, will not raise InvalidEmailError ```
@@ -1430,7 +1475,7 @@ Indeed, there's no guarantee that _any_ error specified ever actually will be. TypeSpec only knows, and can only reason about, errors that are specified in a `@raises` decorator. If an error is specified in a `@handles` decorator but not in any `@raises` decorator of all the model properties that are part of that property or operation, the TypeSpec compiler will not be able to determine whether the error is actually used. -To help developers make that determination, the TypeSpec compiler will issue a warning when this scenario occurs. +To help developers make that determination, the TypeSpec compiler can issue a warning when this scenario occurs. If the developer determines that the error _is_ thrown outside of the context of TypeSpec, they can use the standard [`# suppress` directive][suppress-directive] to suppress the warning. This warning helps to avoid misleading consumers about an error type that may not actually occur. @@ -1455,6 +1500,7 @@ model User { profilePictureUrl: string; } +@handles(PermissionDeniedError) op getUser(id: string): User | NotFoundError; ``` @@ -1480,3 +1526,4 @@ As a result, the TypeSpec compiler will issue a warning. [post-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.post [put-decorator]: https://typespec.io/docs/libraries/http/reference/decorators/#@TypeSpec.Http.put [default-visibility]: https://typespec.io/docs/language-basics/visibility/#basic-concepts +[run-after]: https://learn.microsoft.com/en-us/azure/logic-apps/error-exception-handling#manage-the-run-after-behavior From 42f37140bc835a1afafafd532b01b829148067bd Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Mon, 9 Jun 2025 15:47:29 -0700 Subject: [PATCH 17/18] fix azure logic apps example --- .../model-property-error-handling.md | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index e54e4649f0e..bbacaa0d81a 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -1137,47 +1137,57 @@ patterns that require similar nuance in error handling appear across multiple do This section is meant to demonstrate a few areas in real-world use where this proposal allows a new kind of error handling that is not currently possible. Some may be more esoteric and/or speculative than others, but the goal is to explore a wide spectrum of use cases. -### Azure Logic Apps Workflow Orchestration +### Azure Logic Apps Azure Logic Apps represents a compelling use case for field-level error handling specifications. -Logic Apps workflows consist of multiple actions that can fail independently, -with subsequent actions configured to handle specific failure types through ["run after"][run-after] behavior +Logic Apps workflows consist of multiple actions that can fail independently, with subsequent actions configured to handle specific failure types through ["run after"][run-after] settings. + +Logic Apps uses execution states rather than semantic errors. +Actions can result in `Failed`, `Skipped`, `TimedOut`, or `Successful` states, and subsequent actions can be configured to run after specific combinations of these states. Consider a workflow that retrieves user data and processes it through multiple services: ```typespec +// Logic Apps execution states +@error model Failed { reason: string; } +@error model TimedOut { duration: int32; } +@error model Skipped { condition: string; } + model UserProfileData { - @raises(UserNotFoundError, PermissionDeniedError) + @raises(Failed, TimedOut) // getUserInfo action might fail or timeout basicInfo: UserInfo; - @raises(ServiceUnavailableError, RateLimitExceededError) + @raises(Skipped, TimedOut) // getSocialLinks action might be skipped or timeout socialMediaLinks: SocialLinks; - @raises(NetworkTimeoutError) + @raises(Failed) // getProfileImage action might fail profileImage: ImageData; } -@handles(UserNotFoundError, PermissionDeniedError) +@handles(Failed) // Configure "run after: Failed" op createDefaultProfile(userData: UserProfileData): UserProfile; -@handles(ServiceUnavailableError) +@handles(TimedOut) // Configure "run after: TimedOut" op retryWithBackoff(userData: UserProfileData): UserProfile; + +@handles(Failed, TimedOut) // Configure "run after: Failed, TimedOut" +op logErrorAndContinue(userData: UserProfileData): void; ``` -This TypeSpec definition directly maps to Logic Apps patterns: +This TypeSpec definition maps to Logic Apps execution state patterns: -- **`@raises` decorators** specify which actions in the workflow can produce specific error types -- **`@handles` decorators** correspond to "run after" configurations that execute subsequent actions only for certain failure conditions -- **Error inheritance** mirrors how Logic Apps scopes can handle categories of related failures +- **`@raises` decorators** specify which execution states individual actions can produce +- **`@handles` decorators** correspond to "run after" configurations that execute subsequent actions based on specific execution states +- **Multiple state handling** allows actions to run after combinations of states (e.g., both Failed and TimedOut) When generating Logic Apps workflow definitions from TypeSpec, an emitter could: 1. **Generate appropriate "run after" configurations** based on `@handles` decorators -2. **Create error handling scopes** that catch specific error types from `@raises` specifications -3. **Implement retry policies** tailored to the documented error types -4. **Generate monitoring and alerting** based on the expected error scenarios +2. **Create conditional logic** that routes workflow execution based on action states +3. **Implement retry and error handling patterns** based on the specified execution states +4. **Generate monitoring and alerting** for specific failure patterns -This approach enables Logic Apps developers to model complex error handling scenarios in TypeSpec and generate robust, fault-tolerant workflows with proper error boundaries and recovery mechanisms. +This approach could enable Logic Apps developers to model execution state handling in TypeSpec and generate robust workflows with proper conditional routing based on action outcomes. ### Netflix-style Circuit Breaker Patterns From 5b332a06be8190bd5a1c7cc54b226b4ea6f0368c Mon Sep 17 00:00:00 2001 From: Steve Rice Date: Mon, 9 Jun 2025 15:50:23 -0700 Subject: [PATCH 18/18] additional polish --- .../model-property-error-handling.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/graphql/letter-to-santa/model-property-error-handling.md b/packages/graphql/letter-to-santa/model-property-error-handling.md index bbacaa0d81a..3a3405a34bc 100644 --- a/packages/graphql/letter-to-santa/model-property-error-handling.md +++ b/packages/graphql/letter-to-santa/model-property-error-handling.md @@ -154,7 +154,7 @@ This becomes important when considering error inheritance.
-### Interaction between`@raises` and `@handles` decorators +### Interaction between `@raises` and `@handles` decorators Model properties may have one or more error types defined in both their `@raises` decorator and the `@handles` decorator. In this case, the error is still considered possible at that property. Code emitters should treat `@raises` as taking precedence for code generation and documentation. @@ -184,7 +184,7 @@ Most languages have a way to specify that an error type inherits from another er For the purposes of discussion, let's imagine we have a base error type `GenericError` and two errors that extend it: `NotFoundError` and `PermissionDeniedError`. -#### Error inheritance +`@handles` decorator +#### Error inheritance + `@handles` decorator Error handling is often performed generically based on a base error type, allowing the developer to handle errors that were not known at the time of writing the code. @@ -213,7 +213,7 @@ op getUser(id: string): User; This definition states that the protocol-specific behavior implied by the `@handles(GenericError)` decorator will also apply to `NotFoundError` and `PermissionDeniedError`. -#### Error inheritance +`@raises` decorator +#### Error inheritance + `@raises` decorator The inheritance described above for `@handles` does _not_ apply to the `@raises` decorator. @@ -1137,6 +1137,8 @@ patterns that require similar nuance in error handling appear across multiple do This section is meant to demonstrate a few areas in real-world use where this proposal allows a new kind of error handling that is not currently possible. Some may be more esoteric and/or speculative than others, but the goal is to explore a wide spectrum of use cases. +
+ ### Azure Logic Apps Azure Logic Apps represents a compelling use case for field-level error handling specifications. @@ -1189,6 +1191,8 @@ When generating Logic Apps workflow definitions from TypeSpec, an emitter could: This approach could enable Logic Apps developers to model execution state handling in TypeSpec and generate robust workflows with proper conditional routing based on action outcomes. +
+ ### Netflix-style Circuit Breaker Patterns Microservices architectures use circuit breakers for individual service calls, @@ -1219,7 +1223,9 @@ op getDashboardWithFallbacks(userId: string): UserDashboard; This pattern allows critical errors (billing issues) to propagate while gracefully handling non-critical failures (recommendations, watch history) through fallbacks or cached data. -### E-commerce Partial Product Data +
+ +### E-commerce: Partial Product Data E-commerce platforms need to handle partial product availability where inventory, pricing, and content management systems can fail independently. @@ -1242,7 +1248,9 @@ op getProductPageWithDefaults(productId: string): ProductPage; This enables platforms like Shopify to show "availability unknown" or cached pricing when specific services are down, rather than showing broken product pages. -### Content Management Systems - Progressive Enhancement +
+ +### Content Management Systems: Progressive Enhancement CMS platforms where page components can fail independently but the page should still render with graceful degradation.