DEV Community

joe-re
joe-re

Posted on

Why Does GraphQL Return 200 Even on Errors? A Clear Guide to GraphQL HTTP Status Codes

๐Ÿ‡ฏ๐Ÿ‡ต This article is a self-translated version of my original post written in Japanese, published on Zenn: GraphQLใฏใชใœใ‚จใƒฉใƒผใงใ‚‚200ใ‚’่ฟ”ใ™ใฎใ‹๏ผŸ

Background

One of the most frequently debated topics in GraphQL error handling is: what HTTP status code should the server return?

In REST, the convention is clear: 400 for bad input, 403 for authorization errors, 500 for server errors. The HTTP status code carries the semantic meaning of the response.

GraphQL, however, can return 200 even when the response contains errors.
That said, this does not mean "always return 200 regardless of what went wrong." This tends to confuse developers coming from a REST background.

Historically, application/json was the widely used content type for GraphQL responses, and for compatibility reasons, returning 200 for everything was the safe choice. With the introduction of application/graphql-response+json, however, things have changed โ€” the cases where 4xx or 5xx should be returned are now much more clearly defined.

Since status code design comes up frequently in internal discussions on my team, I'd like to share my own understanding of the current GraphQL over HTTP spec and its historical evolution.


TL;DR

My conclusion: return 4xx/5xx in roughly two situations, and 2xx for everything else.

  • When an error occurs before GraphQL execution begins
  • When a well-formed GraphQL response cannot be constructed

Errors before execution fall into two main categories:

  1. Document syntax errors or failure to identify the operation โ€” the GraphQL request itself is malformed โ†’ 400
  2. Rejection at the HTTP layer before reaching the endpoint โ€” e.g., unauthenticated request โ†’ 401

Failure to construct a GraphQL response typically means a server-side problem (e.g., server crash) where no response body can be generated at all โ†’ 5xx

The GraphQL over HTTP spec also allows servers to add custom validation rules (e.g., depth limits, complexity limits). Whether application-specific input rules fall into this category is not explicitly defined in the spec. Given that the listed examples are things like depth/complexity limits, it seems reasonable to interpret this as covering pre-execution, GraphQL-layer validations rather than business logic errors.


Two Types of GraphQL Errors

GraphQL has a concept called Partial Response.
โ†’ GraphQL Spec: Response

The idea is that even if an error occurs during execution, only the failed part is dropped โ€” the successfully resolved parts are still returned.

The spec distinguishes between two result types: Execution Result and Request Error Result.

Execution Result and Partial Response

โ†’ GraphQL Spec: Execution Result

In an Execution Result, the data field is always present. Even when errors occur, they appear alongside the data in an errors field.

In other words, a response with both data and errors is perfectly normal in GraphQL.

Here's a simple example. Consider the following query where favoriteArticles is nullable:

query {
  users {
    id
    name
    favoriteArticles {
      id
      title
      references { # error occurs here
        id
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If fetching references throws an error, GraphQL propagates null up to the nearest nullable field (favoriteArticles), and the response looks like this:

{
  "data": {
    "users": [
      {
        "id": 1,
        "name": "joe-re",
        "favoriteArticles": null
      }
    ]
  },
  "errors": [
    {
      "message": "Failed to fetch references",
      "path": ["users", 0, "favoriteArticles", 0, "references"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is key: in GraphQL, errors in the response body represents a part of the GraphQL execution result, not an HTTP-level failure.
โ†’ GraphQL Docs: Field Errors

Request Error Result

โ†’ GraphQL Spec: Request Error Result

A GraphQL request returns a request error result when one or more request errors are raised, causing the request to fail before execution. This request will result in no response data.

This covers errors that occur before execution even begins. Critically, the data field is not returned at all in this case.

The spec lists causes such as:

A request error may be raised before execution due to missing information, syntax errors, validation failure, coercion failure, or any other reason the implementation may determine should prevent the request from proceeding.

These are cases where the server decides the request cannot proceed โ€” before GraphQL execution starts.


HTTP Status Codes in GraphQL over HTTP

The above distinction (Execution Result vs. Request Error Result) comes from the core GraphQL spec. How these map to HTTP is defined by the GraphQL over HTTP spec.

โ†’ GraphQL over HTTP: Status Codes

In case of errors that completely prevent the generation of a well-formed GraphQL response, the server SHOULD respond with the appropriate status code depending on the concrete error condition, and MUST NOT respond with a 2xx status code when using the application/graphql-response+json media type.

The key principle here is: whether a well-formed GraphQL response can be generated determines the HTTP status code.

For example, if the response body can't be assembled at all (e.g., the JSON is broken or the server crashed), a well-formed response is impossible โ€” so 2xx is inappropriate.

The spec provides concrete examples for application/graphql-response+json:
โ†’ 6.4.2.1 Examples

Note: The rest of this article assumes application/graphql-response+json. The differences with application/json are covered later.

When to Return 4xx

4xx applies when the request fails before GraphQL execution begins:

  • Document syntax error
  • Document validation failure
  • Operation cannot be determined
  • Variable coercion failure

These are cases where the server can determine upfront that the request cannot proceed. For application/graphql-response+json, the spec recommends 400 Bad Request for these.

Separately, requests that fail at the HTTP layer โ€” such as unauthenticated requests that never reach the GraphQL endpoint โ€” should return 401 or similar.

When to Return 2xx

2xx applies when GraphQL execution proceeded but encountered errors along the way โ€” i.e., when Partial Response applies:

  • An exception thrown inside a resolver
  • A partial field resolution failure
  • A business logic condition that caused processing to fail

In these cases, the failed fields become null in the response. GraphQL's null propagation rules apply: if a non-null field is null, the error propagates up to the nearest nullable ancestor.
โ†’ GraphQL Spec: Handling Execution Errors

When to Return 5xx

5xx applies when a well-formed GraphQL response cannot be returned at all, regardless of pre-execution checks:

  • The server has crashed
  • Middleware fails before reaching the GraphQL handler
  • The response body cannot be generated

Since returning any well-formed GraphQL response is impossible, these are treated as standard HTTP server errors.


What Did Things Look Like Before application/graphql-response+json?

A bit of historical context helps here.

GraphQL originally used application/json as the response content type. During the development of the GraphQL over HTTP spec, a GraphQL-specific media type called application/graphql+json was introduced. However, this name caused confusion with the media type for GraphQL documents (.graphql files), so in 2022 it was renamed to application/graphql-response+json to clearly indicate it's for responses.
โ†’ GitHub PR #215

For legacy compatibility, the official docs recommend including application/json in the Accept header when targeting GraphQL servers that predated 2025:
โ†’ GraphQL: Serving over HTTP

The application/graphql-response+json is described in the draft GraphQL over HTTP specification. To ensure compatibility, if a client sends a request to a legacy GraphQL server before 1st January 2025, the Accept header should also include the application/json media type as follows: application/graphql-response+json, application/json

Timeline summary:

  1. application/json was the de facto standard for GraphQL responses
  2. application/graphql-response+json was proposed in 2022
  3. It is now the preferred response type in the GraphQL over HTTP spec

Why application/json Alone Was Problematic

GraphQL works fine with application/json. But using HTTP status codes meaningfully alongside it had a problem:

When a non-2xx JSON response is received, it's hard to tell if it came from the GraphQL server itself.

A 400 or 500 JSON response might be from:

  • The GraphQL server
  • An intermediary like an API Gateway, reverse proxy, or WAF

From the client's perspective, there's no reliable way to distinguish these. This is why, in the application/json world, always returning 200 was the safer operational choice โ€” and why that pattern persists in many codebases today.


How application/graphql-response+json Solved This

application/graphql-response+json was introduced precisely to address this ambiguity. It is a media type exclusively for GraphQL responses.

The GraphQL over HTTP spec positions it as the preferred response type.

Its key benefit: even when the HTTP status is not 200, the response body can be reliably interpreted as a GraphQL response.

This enables a much cleaner separation of concerns:

  • HTTP status code โ†’ signals transport-level success or failure
  • Response body โ†’ carries GraphQL-level error details

For clients, this means you can more confidently trust the errors field in the response โ€” without worrying about whether the JSON came from GraphQL or from an intermediary.


Summary

In this article, I've shared my understanding of GraphQL status code design, grounded in the GraphQL over HTTP spec.

Since application/graphql-response+json was introduced, the use of 4xx/5xx in GraphQL has become much clearer and better defined.

The key mental model when thinking about GraphQL status codes:

  • Did the error occur before GraphQL execution started? โ†’ 4xx
  • Did execution proceed, but result in an error? โ†’ 2xx with errors in the body
  • Could no GraphQL response be generated at all? โ†’ 5xx

With application/graphql-response+json as your baseline, this distinction becomes very clean.

I hope this helps someone out there navigating the same questions.

Top comments (0)