skip to content

A GraphQL field declares arguments but has no resolver - what happens to those arguments?

level: seniorimportance: should knowfreq 50%

answer

  1. Two stages, and only one of them looks
  2. Coercion happens whether or not code reads it
  3. The generic fallback takes no arguments into account
  4. Filters and limits become documentation
  5. Fixtures smaller than the limit hide it

basics

~20 s

They are validated, defaulted and coerced normally, then handed to the default resolver, which ignores them and returns the parent value's whole same-named member. The filter or limit silently has no effect and no error is reported.

solid answer

~50 s

Argument handling and resolution are separate stages. Execution coerces the field's arguments against their declared definitions - applying defaults, rejecting a missing Non-Null argument or an uncoercible value as a request error - and only then calls the field's resolver. When no resolver was registered, the generic default runs: it reads the same-named member off the parent value and never looks at the arguments. So the request is legal, the response is a normal success with an empty errors list, and the argument is **fully validated and completely inert**. A `SeatMap.seatEvents(since: DateTime, limit: Int = 100)` field served by default resolution returns every event on the loaded aggregate; the SDL's `limit: 100` reads like a guarantee and was never applied. Small fixtures hide it, since a filtered and an unfiltered result are identical below the limit. Defences: a CI check that fails any field declaring arguments with no registered resolver, and differential tests asserting that changing an argument changes the result.

code

graphql · 13 lines
graphql
type SeatMap {
  aircraftRegistration: String!
  seatEvents(since: DateTime, limit: Int = 100): [SeatEvent!]!
}

query RecentSeatChanges($reg: String!, $since: DateTime!) {
  seatMap(aircraftRegistration: $reg) {
    seatEvents(since: $since, limit: 50) {
      occurredAt
      designator
    }
  }
}

go deeper

for a junior

Take away the rule: adding an argument to a field does not make anything read it. Any field with arguments needs its own resolver.

for a middle

Explain the two stages - coercion against the argument definitions, then the resolver call - and why the default ignoring arguments produces a valid response with no errors rather than a failure.

for a senior

Diagnose it from production symptoms: growing payloads and serialisation cost with no error signal, on a field whose arguments were never consumed. Then say how you would prove it with a differential test over a large fixture.

for a principal

Own the systemic fix. A schema check in CI that rejects argument-taking fields with no resolver turns a recurring silent defect into a build failure, which matters most where many teams extend one graph.

## The mechanics first Arguments and resolvers are two separate stages of executing one field, and it is easy to assume the first implies the second. Stage one is entirely the spec's: for the field being executed, execution coerces the argument values. It matches the arguments written in the document (or supplied through variables) against the argument definitions on the schema's field, applies defaults for arguments the client omitted, coerces each value to its declared input type, and raises a request error if a Non-Null argument is missing or a value cannot be coerced. This happens whether or not anything will ever look at the result. Stage two hands those coerced values to the field's resolver. And when no resolver was registered, the generic default one runs — the rule that reads a member of the same name off the parent value. It gets the arguments. It ignores them. So the arguments are **fully validated and completely inert**. The client's request is well-formed, the response is a normal 200-shaped success with no errors list, and the field returns the parent's whole same-named member as though no argument had been written. ## The failure, in a seat-map graph An airline seat-map service exposes an append-only log of seat-inventory changes per aircraft, and the schema declares it with a filter: ```graphql type SeatMap { aircraftRegistration: String! seatEvents(since: DateTime, limit: Int = 100): [SeatEvent!]! } ``` The parent value is a loaded seat-map aggregate that happens to carry a `seatEvents` collection, so default resolution finds a same-named member and returns it. In development that collection holds a couple of dozen entries and every test passes: the client sends `since` and gets back events, and nobody notices that the events it got back were not filtered. In production the list grew without a bound. Eighteen months of reconfigurations, crew blocks and irregular-operations churn later, one long-lived widebody's aggregate carried 41,830 events, and every client asking for "changes in the last hour" was serialising all of them. The symptom was not a wrong answer that anyone could see — clients were rendering the most recent entries and discarding the rest — it was response size, serialisation time and memory pressure climbing on a curve nobody could tie to a code change. The `limit: 100` default made it worse: it read like a guarantee in the SDL, and it was never applied. ## Why it is hard to catch * **Nothing errors.** No validation failure, no field error, no log line. The argument was legal, so the request was legal. * **The SDL lies convincingly.** Reviewers read the field signature and infer behaviour. An argument in the schema is a promise, and the default resolver never made it. * **Small data hides it.** Any test fixture whose collection is smaller than the limit produces identical results filtered or not. The test asserts the events came back, not that filtering happened. * **Wiring is silent.** Most servers do not consider "field with arguments, no resolver" an error at startup; the default is the fallback for every field uniformly. An audit across a 62-subgraph supergraph found nine fields in this state — all of them list fields with a filter or a limit argument that no code consumed. Every one had been added by someone extending a schema next to fields the default was legitimately serving. ## Defences, in order of value 1. **A schema check in CI.** Walk the executable schema and fail the build for any field that declares arguments but has no registered resolver. It is a handful of lines against the built schema and it catches the class outright, not the instance. 2. **A test per argument, not per field.** The assertion that matters is *differential*: two calls with different argument values must return different results. Fixtures must be larger than the limit under test. 3. **Treat the default as a whitelist, not a fallback.** Some servers can be configured to require explicit resolvers, or to log unresolved fields at startup; where that exists, use it on list fields at minimum. 4. **Watch response size by operation.** Field-level metrics and payload-size percentiles per operation name will show the drift long before a user complains, because the failure signature is volume, not errors. ## The generalisation worth saying out loud An argument in a schema is documentation until code reads it. Default resolution serves the fields whose value is already sitting on the parent value, and a field that takes arguments is by definition not one of those: its value depends on something the client said. The moment you add an argument to a field, you have taken it out of the default's domain — and nothing in the type system will tell you so.

  • Are the arguments still validated if nothing consumes them?
    Yes. Coercion runs against the field's declared argument definitions before any resolver is called, so a missing Non-Null argument or an uncoercible enum value is still a request error. That is precisely why the bug is invisible: clients that send well-formed arguments get a well-formed, entirely unfiltered success response.
  • Why do tests usually pass on a field in this state?
    Because they assert presence, not effect, on fixtures smaller than the limit. If the collection holds twenty entries and the test asks for the last hour with a limit of fifty, filtered and unfiltered results are byte-identical. The assertion that catches it is differential: two calls with different argument values must return different results, over a fixture larger than the limit.
  • How would you keep this class of defect out of a large schema rather than fixing instances?
    Make it a build failure. Walk the built executable schema in CI and reject any field that declares arguments with no registered resolver - a handful of lines that catches the class outright. Add per-argument differential tests, prefer servers or settings that report unresolved fields at startup, and watch payload-size percentiles per operation, since the production signature is volume rather than errors.

saying these in an interview costs you the question

  • Says a field with unconsumed arguments raises an execution error
  • Assumes the default resolver applies filter and limit arguments
  • Believes a schema default value is enforced by the server
  • Claims argument coercion is skipped when no resolver reads it
  • Treats the SDL signature as proof the filtering happens
  • Expects small test fixtures to reveal the missing filter

context