Marks a GraphQL OBJECT type as the typed home for one or more Java exceptions. When a @service-returned payload exposes an errors: field of an @error type (or a union of @error types), the rewrite generates an error channel: at runtime the service method’s exception is caught, matched against the type’s handlers:, and the matched type is appended to the payload’s errors list.
@error is the entry point to Graphitron’s "errors-as-data" path. It contrasts with throwing: a thrown exception ends the resolver; a mapped exception flows through the GraphQL response as a typed error alongside the rest of the data.
SDL signature
directive @error(handlers: [ErrorHandler!]!) on OBJECT
input ErrorHandler {
handler: ErrorHandlerType! # GENERIC | DATABASE | VALIDATION
className: String # FQCN of the exception (required for GENERIC; rejected for DATABASE and VALIDATION)
code: String # Vendor-specific DB error code (DATABASE only)
sqlState: String # Standardized SQL state (DATABASE only)
matches: String # Substring the exception message must contain
description: String # Message returned to the client instead of the exception's own (rejected for VALIDATION)
}
enum ErrorHandlerType { GENERIC, DATABASE, VALIDATION }
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
|
|
(required) |
One or more handler entries describing which Java exceptions map to this error type. Multiple entries on a single type are an "OR" — any matching entry assigns the exception to this type. The first entry whose criteria match (across all |
ErrorHandler fields:
| Field | Required | Description |
|---|---|---|
|
yes |
Selector. |
|
for |
Fully qualified exception class name. Required for |
|
no ( |
Vendor-specific error code as returned by |
|
no ( |
Standardized SQL state from |
|
no |
Substring the exception message must contain. Useful to narrow a broad |
|
no |
The message the client reads on |
description: only changes what message: reads. The matched exception itself is what lands in the payload’s errors list, so extra fields keep reading the live exception and a union still resolves __typename from the exception’s own class.
Canonical example
A service payload with a typed errors channel:
# FilmPayload is backed by com.example.FilmPayload, the return type of the
# createFilm @service method below; no directive declares the binding.
type FilmPayload {
film: Film
errors: [FilmError]
}
union FilmError = YearOutOfRange | NotAllowed | DbError
type YearOutOfRange @error(handlers: [
{handler: DATABASE, sqlState: "23514", matches: "year_check",
description: "Release year must be between 1901 and 2155"}
]) {
path: [String!]!
message: String!
}
type NotAllowed @error(handlers: [
{handler: GENERIC, className: "com.example.NotAllowedException",
description: "You are not allowed to do this"}
]) {
path: [String!]!
message: String!
}
type DbError @error(handlers: [
{handler: DATABASE, sqlState: "23503"}
]) {
path: [String!]!
message: String!
}
type Mutation {
createFilm(input: CreateFilmInput!): FilmPayload @service(service: {className: "com.example.FilmService"})
}
When the service method throws an exception carrying a java.sql.SQLException whose getSQLState() is 23514 and whose message contains year_check (jOOQ’s DataAccessException wrapping the driver’s exception is the usual shape), the rewrite catches it, resolves the YearOutOfRange type with message = description (or the exception’s own message when description: is absent), and appends the matched exception to FilmPayload.errors. The data field still streams to the client; only film is null for that request.
VALIDATION handles a Bean-Validation failure raised before the service method runs. It takes no other field on the entry, description: included:
type ValidationErr @error(handlers: [{handler: VALIDATION}]) {
path: [String!]!
message: String!
}
Extra fields
Beyond the required path and message, an @error type may declare extra fields. Each extra field is read from the matched exception (or, for VALIDATION, the GraphQLError) through an accessor matching the field name: code: String reads getCode(), code(), or a public code field. When the Java accessor name diverges from the GraphQL field name, @field(name:) names the accessor to use instead:
type FilmLookupInvalid @error(handlers: [{
handler: GENERIC,
className: "no.sikt.graphitron.rewrite.test.services.FilmLookupInvalidIdException"
}]) {
path: [String!]!
message: String!
attempted: Int @field(name: "attemptedId") # reads getAttemptedId()
}
The build fails when a declared extra field cannot be populated from every handler’s source class, listing the accessors the class does expose and naming the type the accessor had to return. @field on path or message is rejected: those two fields are populated by Graphitron itself.
Publishing a node id from an extra field
An extra field carrying @nodeId(typeName:) publishes a global object ID built from what the read yielded, so the exception carries the key and the client sees the ID:
type FilmLookupInvalid @error(handlers: [{
handler: GENERIC,
className: "no.sikt.graphitron.rewrite.test.services.FilmLookupInvalidIdException"
}]) {
path: [String!]!
message: String!
attempted: Int @field(name: "attemptedId") # the raw key
attemptedFilm: ID @nodeId(typeName: "Film") @field(name: "attemptedId") # the same value as a node id
}
Both fields read getAttemptedId(). The accessor therefore has to return film_id’s own Java type, not a `String: the encode happens on the way out, so an accessor that already returns an encoded ID is a build error naming the type it should have returned. If you were encoding by hand to publish an ID here, that call site goes away and the accessor exposes the key.
typeName: is required, and the node type’s key must be one column. An @error type stands for no table, so there is nothing for a bare @nodeId to inherit its node from; and the field is one value, so a node type keyed on several columns has nothing to be encoded from and the build says so, naming the count.
Constraints
-
@erroronly takes effect when the type appears as (or in a union behind) anerrors:field on a payload reachable from a@servicefield. A standalone@errortype with no carrier is rejected at classify time. -
Declaration order decides, and the first match wins. Handlers are walked in SDL order (the channel’s
@errortypes in declaration order, and within a type itshandlers:in array order), so when two handlers both match one throwable the one declared first is the answer. Declare the narrow handler before the broad one. -
Nothing sorts by specificity.
matches:makes two otherwise-identical handlers distinct:(GENERIC, IllegalArgumentException, matches: "foo")and(GENERIC, IllegalArgumentException)(nomatches) are both reachable, but the narrowed one runs first only if you declared it first. Declaring the broad one first leaves the narrow one silently unreachable. -
Two handlers of the same variant with identical criteria (same
handler,className,code,sqlState,matches) are rejected: the second would be unreachable. Overlap across variants is allowed, since aGENERIChandler and aDATABASEhandler inspect orthogonal fields. -
For
GENERIC,className:is required. The build rejects aGENERIChandler without it. -
For
DATABASE, supply at least one ofcode:,sqlState:, ormatches:(otherwise everySQLExceptionon the channel routes to this type, which is rarely the intent). -
code:andsqlState:cannot be combined: the build rejects aDATABASEentry carrying both, since the two discriminators are vendor-conflicting. Split into two entries, one per discriminator. Vendor portability: prefersqlState(PostgreSQL is specific, Oracle is generic), narrowed with amatches:substring where needed. -
An extra field carrying
@nodeIdneedstypeName:and a node type keyed on a single column; see Publishing a node id from an extra field. -
Applies only to
OBJECT. Interfaces and unions cannot carry@errordirectly — the union-of-error-types pattern works because the union members are individually@error-decorated.
See also
-
@serviceis the upstream of the errors channel: only service-returned payloads carry one. -
The payload type that exposes the
errors:field is class-backed, its backing class inferred from the@servicemethod’s return type. -
How-to: The errors channel covers union vs single-type carriers, how
message:resolves betweendescription:and the source’s own message, and thepath:slot’s contents.