Typed error result unions
A fallible field normally reports failure as a GraphQL error. That is the right shape for an
unexpected failure, but some failures are part of the API: a createUser mutation may fail
because the address is taken, and the client needs a typed ConflictError payload it can switch
on, not a message in the errors array.
A field can return Result<T, E> where E is an enum derived with ErrorUnion. The builder
generates a result union from the Rust types:
Ok(value) resolves to MutationCreateUserSuccess { data: value }, and each Err variant
resolves to its own object type. No GraphQL errors entry is produced for a typed failure; it is
ordinary response data.
Full example
A client selects the success payload and the error payloads it understands:
The response has no errors array; the result is the ConflictError object.
API
Each has a *_with_info form that appends &gql::ResolveInfo<'_>, like the other resolver
variants. Root methods work for query, mutation and subscription roots; the generated names use
the configured root name.
The generated type names are:
<Parent><Field>Success, for exampleMutationCreateUserSuccess<Parent><Field>Result, for exampleMutationCreateUserResult
<Parent> is the object's actual GraphQL name, including one created with
Schema::object_ref, so AdminUser.createUser produces AdminUserCreateUserSuccess. The field
name is PascalCased: createUser becomes CreateUser, and create_user also becomes
CreateUser.
Reopening an object or building the same schema more than once is idempotent: the generated types are identified by name and declaration, not appended again. A second declaration of the same field is still the usual duplicate-field build error.
Success payload rules
The success member always has exactly one field:
data's type comes from T:
T = StringgivesString!,T = i32givesInt!.T = Vec<User>gives[User!]!.T = Option<User>givesUser;Noneis a validdata: null.T = SearchResult(a derivedUnion) givesSearchResult!; the abstract value still resolves through each concrete object'sis_type_of.- A custom
OutputTypecan declare an interface or another abstract type, and the value it converts to still resolves normally.
Only an Option loosens the success payload's non-null marker. A bare object model is nullable
as a field type in gurafi generally, but a result union's success payload is present whenever
the resolver returns Ok, so data is non-null.
Registering error payloads
Every variant holds one object model. The derive validates the enum's shape at compile time:
The payload objects are not registered by the derive. They are ordinary object types, so their fields and resolvers are declared where the rest of the schema is:
Forgetting one is a build error that names the field and the missing payload:
The derive rejects generics, unit variants, struct variants, tuple variants with zero or several fields, and the same payload type in two variants. A GraphQL union cannot include the same object twice, so that last rule is enforced before the schema is built.
Errors, codes and masking
A typed error payload is data, not a GraphQL error, so it is never masked by error formatters. The
payload is whatever object model you define; a code field is the usual way to give clients a
stable discriminator:
gurafi deliberately does not synthesize code, extensions or any other envelope. The payload
is yours, and the same object can be reused by several fields.
Unexpected failures still use normal GraphQL errors. Keep those two paths separate: return
Err(CreateUserError::...) for the failures the API documents, and a GraphQLError (through
field_result, authorization, a custom scalar, or an internal error) for everything else.
Error masking remains a server concern. suku's Server::format_error runs over GraphQL errors
before they reach the client; typed error payloads are response data and are not touched by it.
Do not put internal diagnostics in an error payload just because it is typed.
How the Rust pieces fit
Error payloads are registered like any other object, so their fields, authorization and resolvers live with the rest of the schema. A missing payload is a build error rather than an execution-time surprise.
Limitations
- The error enum cannot be generic. Use a concrete enum and concrete payload objects.
- Error payload types are used under their
ObjectModel::NAME. A runtime-named error payload (object_ref) is not picked up automatically; register it under the name the enum's payload model declares, or wrap the runtime-specific data in a concrete derived object. - Generated success objects are merged into the schema before build-time plugins run, so field
metadata plugins see the
datafield. They do not participate in object-level authorization declared on the parent, because they are not a user model. - A field exposes exactly one success type per declaration. To return two different success
shapes, use two fields or put the alternatives in
T(for example a derived union).
