GraphQL

Validation, Execution, And The Response

The Pipeline At A Glance

  1. Parse the document into an AST. Syntax errors stop here.
  2. Validate the AST against the schema. Nothing user-defined runs yet.
  3. Execute by walking the operation's selection set, calling a resolver for each field.
  4. Respond with a JSON object containing data, and errors when anything went wrong.

Validation: Rejected Before Any Code Runs

Validation checks the whole document against the schema and either passes it entirely or rejects it entirely. Among the rules:

  • every selected field must exist on the type it is selected from,
  • scalar fields must not carry selection sets; object fields must,
  • arguments must exist, be correctly typed, and include all required ones,
  • variables must be declared, used, and of types compatible with where they are plugged in,
  • fragments must be spread on compatible types, must be used, and must not form cycles.

The payoff: a typo like selecting Album.titel fails with a precise error before a single resolver or database call happens. Your resolver code can assume every request it sees is structurally sound. This is also why validation errors always mean a broken client build or a schema change — they are worth alerting on differently from runtime errors.

Execution: A Resolver Per Field

Execution walks the selection set top-down. Every field is backed by a resolver function receiving four things — in webonyx/graphql-php the signature looks like:

PHP example
function (mixed $parent, array $args, AppContext $context, ResolveInfo $info): mixed
  • $parent — the value the enclosing field resolved to. For root fields it is your configured root value; for Album.artist it is the album.
  • $args — the field's arguments, already validated and coerced (['first' => 3]).
  • $context — one request-scoped object you provide: the current user, database connections, loaders. This is the channel for request state; resolvers should not reach for globals.
  • $info — metadata about the selection (field name, path, what sub-fields were requested), useful for optimizations.

A resolver for our catalog might be:

PHP example
'artist' => function ($root, array $args, AppContext $ctx): ?array {
    return $ctx->artists->findBySlug($args['slug']);
},

Most fields need no explicit resolver at all: the default resolver looks up a matching property or array key on $parentAlbum.title just reads $album['title']. You write resolvers where work happens: root lookups, relationship traversal, computed values.

Two execution rules worth knowing: sibling fields under Query may run concurrently while root Mutation fields are strictly serial (as the mutations lesson covered), and results always come back in the order the query requested, regardless of the order in which resolvers finished.

One quiet step happens after each resolver returns: scalar coercion. The engine converts the resolver's raw return value into the field's declared scalar type — a numeric string returned for an Int field becomes an integer, 1 for a Boolean becomes true, and a value that cannot be coerced becomes a field error. Resolvers therefore do not need to hand back perfectly typed values, but coercion failures surface as runtime errors, so sloppy resolver types are still bugs waiting in the data.

Null Propagation: How Errors Stay Contained

When a resolver throws, GraphQL does not abandon the whole request. It records an error and sets that field to null. Then nullability decides the blast radius:

  • if the field is nullable, the null sits there and the rest of the response is intact;
  • if the field is non-null, null is not a legal value — so the parent object becomes null instead, and if that position is also non-null, the null keeps climbing until it reaches a nullable ancestor (or nulls out data entirely).

Concretely: Album.averageRating: Float failing costs you one number. Album.averageRating: Float! failing destroys the whole album object in the response. This is the mechanical reason the schema lesson advised nullable types for fields backed by flaky dependencies.

The Response Format

The spec fixes the top-level response shape:

{
  "data": {
    "album": {
      "title": "Glasswork",
      "averageRating": null
    }
  },
  "errors": [
    {
      "message": "Stats service timed out.",
      "path": ["album", "averageRating"],
      "locations": [{ "line": 4, "column": 5 }],
      "extensions": { "code": "UPSTREAM_TIMEOUT" }
    }
  ]
}
  • data — the result tree, mirroring the query. Present (possibly null) whenever execution began.
  • errors — a list of error objects, each with a message, usually a path pointing at the field that failed, and an optional extensions map for machine-readable details like error codes. Validation failures produce a response with errors and no data at all.
  • extensions (top level) — an optional server-defined map for metadata such as tracing or cost information.

The coexistence of data and errorspartial success — is a defining GraphQL behavior. A client must always check both; "HTTP 200" tells you nothing. The error-handling lesson later covers designing for this deliberately.

What To Check

Before moving on, make sure you can:

  • name the four pipeline stages and what each one can reject or produce
  • list several validation rules and explain why validation failures need no resolver execution
  • describe the four resolver parameters and the role of the context object
  • explain what the default resolver does and when you must write an explicit one
  • trace null propagation through nested non-null fields for a given failing resolver
  • read a response containing both data and errors and identify exactly what failed

What You Should Be Able To Do

After this lesson, you should be able to reason like the engine: given a schema, a query, and a description of which resolver fails, predict the exact response — which fields survive, where nulls land, and what appears in errors — and explain to a teammate why a non-null field turned a small failure into a large one.