GraphQL Error Handling: Separating Business Errors, Transport Errors and Extensions
AI generated
{ }
type
GraphQL · Error Handling · Business Errors · Extensions · Magento
GraphQL Error Handling:
Separating Business Errors, Transport Errors and Extensions

GraphQL has no HTTP equivalent for business errors. Anyone who models business errors as resolver exceptions, fails to distinguish transport errors from network problems, and ignores extensions.category ends up with error handling that forces the frontend to react to heuristics instead of structure.

14 min read Business Errors · Validation Errors · extensions.category · Partial Success GraphQL · Magento 2 · API Design

1. The Three Error Levels in GraphQL

GraphQL distinguishes three fundamental error levels that require different reactions: transport errors at the HTTP level (network failure, server unreachable, HTTP 500), validation errors at the protocol level (query syntax wrong, field does not exist in the schema), and business errors in the resolver layer (product no longer available, missing permission, validation of an input value fails). Anyone who mixes up these three levels writes error handling that produces no meaningful reaction in certain scenarios.

What makes GraphQL special: all three error types can occur in the same HTTP 200 response. A query that partially succeeds can return data fields with values and, at the same time, an errors array with errors for individual fields, the so-called partial success scenario. This is a fundamental difference from REST, where a response signals either success or failure, but never both at once.

2. Transport Errors: Network, HTTP and GraphQL Protocol Errors

Transport errors are errors that never bring the GraphQL request to the execution level at all. These include network failures (no connection to the server), HTTP errors (HTTP 503 Service Unavailable, HTTP 429 Rate Limited), and severe server errors (HTTP 500 signaling a PHP Fatal Error). These errors do not appear in the errors array of a GraphQL response, because no valid GraphQL response comes back at all: the HTTP client instead receives a network error or a non-200 status code.

The practical implication: error handling on the client side must cover both layers. First check whether an HTTP response arrived at all and whether the status code is 200. Then check whether the JSON response contains an errors array. Clients that only check the errors array miss transport errors. Clients that only check HTTP status codes miss GraphQL business errors. Both layers must be handled explicitly.


# HTTP-level errors (NOT in GraphQL errors array):
# HTTP 503: Server unavailable (Magento maintenance mode, Docker restart)
# HTTP 429: Rate limiting (too many requests from one IP)
# Network error: no response at all

# GraphQL-level validation error (in errors array, data is null):
{
  "errors": [
    {
      "message": "Cannot query field \"nonexistent\" on type \"Customer\".",
      "locations": [{ "line": 3, "column": 5 }],
      "extensions": { "category": "graphql" }
    }
  ]
}

# Note: validation errors occur BEFORE resolver execution
# The resolver never runs for an invalid query
# data will be null for validation errors

3. Validation Errors: Schema Violations Before Execution

Validation errors occur when a query violates the schema: a field does not exist, an argument has the wrong type, a non-nullable field is missing in the input type. These errors are detected by the GraphQL engine before a single resolver is called. The result is a response with data: null and one or more entries in the errors array with extensions.category: "graphql".

In practice this means: validation errors are always client-side programming errors, the client sends a query that does not match the schema. In production systems, validation errors should not occur if code generation and CI checks are set up correctly. During development, however, they are valuable feedback: they show precisely which fields or types do not exist in the schema, without a resolver ever starting.

4. Business Errors: Business Logic Failures in the Resolver

Business errors occur during resolver execution and represent states of the business logic: a product is sold out, a promo code has already been used, a minimum order value was not reached. In Magento, these errors are thrown as typed exceptions, which the GraphQL framework converts into structured error entries in the errors array. Unlike transport errors, business errors can coexist with partially valid data.

The fundamental design problem: modeling GraphQL business errors as exceptions makes them poorly typed. The frontend does not know which business errors are possible for a mutation until it reads or tests the API code. The types in the schema describe success scenarios, not error states. The more modern approach, union types as an error model, solves this problem but requires schema changes and is not implemented by default in Magento.

5. extensions.category: Structured Error Categorization

The extensions field in GraphQL errors is an open object that can contain arbitrary machine-readable additional information. Magento uses this field with a category property that classifies the type of error. The most important categories: graphql for schema validation errors, graphql-authorization for missing permissions, graphql-authentication for missing identity, graphql-input for invalid input values, and graphql-no-such-entity for resources that were not found.

This categorization is the key to meaningful error handling on the frontend. Instead of reacting to error message text (which can change and is localized), the frontend reacts to the machine-readable category. graphql-authorization triggers a login redirect, graphql-no-such-entity shows a 404-like message, graphql-input marks the corresponding form field as invalid. This makes error handling stable against text changes and localization.


# Magento GraphQL error response, structured with extensions.category

# Authentication error (no token):
{
  "errors": [{
    "message": "The current customer isn't authorized.",
    "extensions": { "category": "graphql-authentication" }
  }],
  "data": { "customer": null }
}

# Not found error:
{
  "errors": [{
    "message": "No such entity with id = 99999",
    "extensions": { "category": "graphql-no-such-entity" }
  }],
  "data": { "product": null }
}

# Input validation error:
{
  "errors": [{
    "message": "Please enter a valid email address (Ex: johndoe@domain.com).",
    "extensions": { "category": "graphql-input" }
  }],
  "data": { "createCustomer": null }
}

# Frontend error handling based on category (not message text):
# switch (error.extensions?.category) {
#   case 'graphql-authentication': redirectToLogin(); break;
#   case 'graphql-no-such-entity': show404(); break;
#   case 'graphql-input': markFieldInvalid(error.message); break;
# }

6. Partial Success: Data and Errors at the Same Time

Partial success is one of the unintuitive but powerful features of GraphQL: a request can partially succeed, some fields resolve, others fail. The result is a response that contains both a populated data object and a non-empty errors array. The frontend must process both in parallel and decide which partial response can be shown and which error messages are presented to the user.

A concrete scenario in Magento: a query loads product data and customer data at the same time. The product part succeeds, but the customer resolver fails because the token has expired. Partial success means: data.products contains valid product data, data.customer is null, errors contains an entry with category: graphql-authentication. The frontend can show the product page and render the auth error message in parallel, instead of treating the entire page as an error.

7. Union Types as an Error Model: The Better Path for Business Errors

The GraphQL community pattern for typed business errors is union return types in mutations. Instead of a mutation that throws an exception on error, the mutation returns a union type that contains either the success type or a dedicated error type. This makes error states an explicit part of the schema and enables the frontend to implement type-safe error handling.

In practice this looks like this: an addToCart mutation returns AddToCartResult, a union of CartItems and possible error types such as OutOfStockError and MinimumOrderError. The frontend can check with __typename which type the response has and react accordingly. This pattern is not built into Magento but can be implemented in custom modules and mutations, and it is the recommended option for new modules.

8. Error Models Compared

The choice of error model affects how type-safe and maintainable error handling on the frontend is. Both approaches have their justification, the choice depends on the schema context.

Error Model Type Safety Schema Documentation Magento Support
exceptions.category String-based, weak Not visible in the schema Built in, standard
Union return types Strong, __typename based Fully documented in the schema Only in custom modules
Generic errors None None Always possible
Nullable fields Implicit (null = error) Weak (null = error or empty) Always available
Error-as-data pattern Strong, fully typed Explicit in the schema Only in custom modules

For existing Magento code, extensions.category is the pragmatic standard, stable and well documented. For new Magento modules with complex business logic, the effort of union return types is worth it, because they make frontend error handling considerably more robust.

9. Frontend Evaluation: Distinguishing Error Types Programmatically

Clean frontend error handling for GraphQL consists of three layers. Layer one: check the HTTP transport, no response or a non-200 status code are network or server errors that must be handled independently of GraphQL error structures. Layer two: check the errors array and categorize by extensions.category. Layer three: for partial success scenarios, decide which parts of the response are still usable despite the error.

A common mistake on the frontend is treating all GraphQL errors as equivalent and showing a generic error message across the board. This is not helpful for users: an expired session token requires a login redirect, a sold-out product requires a specific product message, a network error requires a retry hint. The error structure of GraphQL, and specifically extensions.category, is made to enable these distinctions programmatically. Anyone who does not use it has to match on error message text, which breaks immediately in multilingual shops.


# Partial success example: products loaded, customer failed
# Single query, mixed result (both data and errors present)

query DashboardData {
  products(search: "laptop", pageSize: 3) {
    items { sku name }
  }
  customer {
    firstname
    orders { items { order_number } }
  }
}

# Response when customer token is expired:
# {
#   "data": {
#     "products": { "items": [{ "sku": "...", "name": "..." }] },
#     "customer": null
#   },
#   "errors": [{
#     "message": "The current customer isn't authorized.",
#     "path": ["customer"],
#     "extensions": { "category": "graphql-authentication" }
#   }]
# }

# Frontend logic:
# 1. data.products is valid, render product section
# 2. errors[0].path = ["customer"], only customer section failed
# 3. errors[0].extensions.category = "graphql-authentication", trigger re-auth
# 4. Do NOT discard entire page because of partial failure

10. Summary

GraphQL error handling requires thinking in three levels: transport errors (no HTTP 200), validation errors (schema violation before execution), and business errors (resolver logic). Magento's extensions.category field makes business errors machine-readable and enables the frontend to react to error categories without text matching. Partial success, the simultaneous return of data and errors, is a strength of GraphQL, but requires explicit partial success handling on the frontend.

For new modules, the union return type pattern is the better choice, because it integrates error states into the schema itself and is fully typed. For existing Magento code, the extensions.category approach is the pragmatic standard. In both cases the same rule applies: generic error handling that treats all errors the same is not sufficient error handling for production GraphQL APIs.

GraphQL Error Handling: The Essentials at a Glance

Three Error Levels

Transport errors (HTTP), validation errors (schema) and business errors (resolver) are different levels with different reactions. All three must be handled explicitly.

extensions.category

Magento standard for machine-readable error categories. The frontend reacts to the category, not to error message text, stable across translations and text changes.

Partial Success

Data and errors can occur simultaneously in a single GraphQL response. errors[].path shows which part of the query failed. Do not discard the entire page.

Union Types for New Development

Use union return types for mutations in new modules, fully typed error states in the schema, without exceptions.category conventions.

11. FAQ: GraphQL Error Handling, Business Errors, Transport Errors and Extensions

1Difference between business errors and transport errors?
Transport errors: HTTP errors, no GraphQL response. Business errors: during resolver execution, in the errors array of an HTTP 200 response.
2What does extensions.category mean in Magento?
Machine-readable error category: graphql-authorization, graphql-authentication, graphql-input, graphql-no-such-entity. Frontend reacts to the category, not the text.
3What is partial success in GraphQL?
Valid data and errors in one response at the same time. errors[].path shows which part failed. Do not discard the entire page.
4How do I distinguish validation errors from business errors?
Validation errors: extensions.category 'graphql', data is null, before resolver execution. Business errors: specific category, data can be partially populated.
5Why react to extensions.category instead of error text?
Error texts change and get localized. extensions.category is stable, language independent and made for programmatic reaction.
6What are union return types and why are they better?
Error states as an explicit part of the schema. Frontend distinguishes type-safely via __typename, without errors array conventions.
7How do I process partial success responses?
errors[].path shows the failed fields. Only discard the affected UI parts, still show the valid parts.
8How do you throw typed business errors in Magento?
GraphQlAuthorizationException, GraphQlNoSuchEntityException, GraphQlInputException, Magento converts them automatically into structured errors with extensions.category.
9Does a business error always abort the entire mutation?
Typically yes, mutations are atomic operations. For batch mutations with independent operations, partial success can make sense.
10How do you test GraphQL error behavior?
Magento integration tests with expectExceptionMessage(). Frontend: GraphQL mocking with controlled error responses and different extensions.category values.