DevTools Hub

Search tools

Search for a developer tool

Common GraphQL Errors

Part of the GraphQL Toolkit

GraphQL errors split into three layers, and knowing which one you're looking at changes where to look for the fix. A parse error means the document isn't even syntactically valid GraphQL — a missing brace, an unterminated string. A validation error means it parses fine but breaks a rule of the GraphQL spec itself, or doesn't match the schema — these are the ones below, and every one of them is caught before a resolver ever runs. A runtime error means validation passed and a resolver threw while actually executing. This post is entirely about the middle layer.

Cannot query field "X" on type "Y".

query {
  user {
    emial   # typo — the field is "email"
  }
}

The single most common GraphQL validation error, and almost always a typo or a stale assumption about the schema — a field that got renamed or removed since the query was written. It requires a schema to catch: without one, there's nothing to check the field name against, which is why this specific error only ever shows up once you validate against real type definitions rather than checking the query's syntax alone.

Field "X" argument "Y" ... is required, but it was not provided.

query {
  user {          # user(id: ID!) requires "id" — none given
    name
  }
}

A required argument (marked ! in the schema, with no default value) has to be supplied at every call site, every time. Unlike an unknown field, this one is easy to introduce by editing the schema after queries already exist elsewhere — adding a new required argument to a field silently breaks every existing query against it, with no warning until each one is re-validated.

Variable "$x" is never used in operation "Y".

query GetUser($id: ID!, $includeEmail: Boolean) {
  user(id: $id) {
    name   # $includeEmail declared above, never referenced below
  }
}

A variable declared in the operation signature has to actually appear somewhere in the selection set. This one is harmless to run — servers execute the query anyway — but it's a real signal of drift: a variable left behind after a field that used it (often behind an @include/@skip directive) was deleted.

Variable "$x" is not defined by operation "Y".

query GetUser {
  user(id: $id) {   # $id used here, never declared in ($id: ID!)
    name
  }
}

The mirror image of the previous one, and the one that actually breaks execution: a variable referenced in the body but missing from the operation's own signature. Common cause — copying a field that uses a variable into a query that never declared it.

There can be only one variable/operation/fragment named "X".

The same wording covers three separate uniqueness rules — a duplicate variable name in one operation's signature, a duplicate operation name in the same document, or a duplicate fragment name in the same document. All three come from the same root cause in practice: two queries or fragments merged into one file (often by a build step that bundles.graphql files together) without checking for name collisions between them.

This anonymous operation must be the only defined operation.

{ me { name } }

query GetPost {
  post(id: "1") { title }
}

The { ... } shorthand for a query is only legal when it's the entire document. The moment a second operation exists anywhere in the same document — named or not — every anonymous operation has to be given a name instead. This surfaces most often when hand-written ad-hoc queries accumulate in the same.graphql file as properly named ones.

Cannot spread fragment "X" within itself.

fragment UserFields on User {
  name
  friend {
    ...UserFields   # spreads itself, directly or via a chain
  }
}

A fragment cycle — A spreads B spreads A, or a fragment spreading itself directly as above. GraphQL rejects this outright rather than trying to resolve it, because a self-referential fragment has no finite expansion: there's no depth at which it would ever stop needing another copy of itself.

Fragment "X" is never used.

A fragment defined in the document but never spread anywhere with ...FragmentName. Almost always leftover from a deleted query — the fragment it supported is gone, but the fragment definition itself didn't get cleaned up with it.

The directive "@X" can only be used once at this location.

query GetUser($a: Boolean!, $b: Boolean!) {
  user {
    name @include(if: $a) @include(if: $b)   # same directive, same field, twice
  }
}

Most built-in and custom directives are non-repeatable — the same directive can only apply once to a given field, argument, or fragment spread. Two separate @include calls on the same field isn't "apply both conditions"; it's rejected before either one would matter. If two conditions genuinely both need to gate the same field, they belong inside a single boolean expression passed to one @include, not two separate directive applications.

The formatter gotcha: comments vs. description strings

# This comment explains the query    <- dropped on format
"""This description explains the field"""   <- preserved on format
type Query {
  user: User
}

Not a validation error, but a genuine surprise the first time it happens: reformatting a GraphQL document through the reference parser (parse → print, which is what any correctness-preserving formatter has to do) silently drops every #-style comment. They're not part of the AST at all — graphql-js discards them during parsing, so there's nothing left to print back out. A """triple-quoted block string""" used as a description in SDL is a completely different thing — a real AST node — and survives formatting intact. The two look similar at a glance and behave nothing alike.

The one this post won't catch: N+1 resolvers

Every error above is something a validator can catch before a query ever runs. The most infamous GraphQL performance problem isn't one of them: a resolver that fetches its own data independently for every item in a list turns one query into N+1 round trips to your data source, and no schema or query validator will ever flag it — it's a property of how the resolver is implemented, not of the query's shape. See What Causes N+1 Queries? for the general pattern and the batching-loader fix that applies here too.

Try it yourself

GraphQL Query Validator runs every rule above that doesn't need a schema — unused/undefined variables, duplicate names, fragment cycles, repeated directives — on the query alone, and adds unknown-field and wrong-argument checks the moment you paste in your schema's SDL alongside it. GraphQL Formatter re-prints a query, mutation, or SDL schema through the same reference parser, so the output is guaranteed syntactically valid — and now you know exactly what it will and won't keep from your comments. Both run entirely in your browser.

Related tools