structured errors instead of "Internal server error"
Well thought out GraphQL error handling decides whether a frontend can react to individual problems in a targeted way or only shows a generic message for every error. Anyone who consistently uses custom exception classes, error extensions and partial responses gives frontend teams the information they actually need for good error messages.
Table of Contents
- 1. Why GraphQL error handling works differently than REST
- 2. The structure of a GraphQL error object in Magento
- 3. The built in GraphQL exception classes
- 4. Custom exception classes and error categories
- 5. Partial responses: data and errors at once
- 6. Error extensions for structured client logic
- 7. Logging and monitoring GraphQL errors
- 8. Consuming errors in the frontend
- 9. Bad vs. good error handling compared
- 10. Summary
- 11. FAQ
1. Why GraphQL error handling works differently than REST
With a REST API, the HTTP status code signals success or failure of a request, a 404 or 500 immediately makes clear that something went wrong. GraphQL works fundamentally differently: almost every response comes back with HTTP status 200, regardless of whether the query succeeded fully, partially, or not at all. The actual GraphQL error handling happens inside the response body itself, through a separate errors array next to the data field.
This peculiarity regularly surprises developers coming from the REST world. A frontend that only checks the HTTP status misses errors completely, because even a response with failed partial requests technically comes back as a success (status 200). Clean GraphQL error handling in Magento therefore means actively evaluating the errors array and writing resolvers so that they produce meaningful, structured error objects instead of generic messages.
A second difference concerns granularity: a single GraphQL query can request several fields at once, some of which can succeed while others fail. This possibility for partial responses is one of the most powerful aspects of GraphQL, but it makes error handling more complex than the binary success or failure model of REST.
2. The structure of a GraphQL error object in Magento
Every error in the errors array of a Magento GraphQL response follows the GraphQL specification: a required message field with human readable text, an optional locations array with line and column in the query, a path array specifying the exact field path in the query tree, and an extensions object for additional structured metadata. Magento uses extensions.category to classify errors into categories such as graphql-input, graphql-authorization, or graphql-no-such-entity.
{
"errors": [
{
"message": "Rating must be between 1 and 5.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["createProductReview"],
"extensions": {
"category": "graphql-input"
}
}
],
"data": {
"createProductReview": null
}
}
This structure is the foundation of any well thought out GraphQL error handling: the frontend can use extensions.category to distinguish whether a user made an invalid input, whether a permission is missing, or whether a referenced entity does not exist, and show different UI reactions depending on the category. Without this categorization, every error ends up in the same generic error message, regardless of the actual cause.
3. The built in GraphQL exception classes
Magento already provides matching exception classes for the most common error cases, which automatically set the correct extensions.category. GraphQlInputException covers invalid client input and becomes graphql-input. GraphQlAuthorizationException signals missing permissions and becomes graphql-authorization. GraphQlNoSuchEntityException is thrown when a referenced entity, for example a product or an order, cannot be found, and results in graphql-no-such-entity.
The decisive advantage of these built in classes is that they are used consistently across the entire Magento core. A frontend that once evaluates the categories for the native createCustomer mutation can reuse the same logic for a custom mutation, as long as that mutation also uses the same exception classes. Anyone who instead throws generic \Exception objects loses this consistency and forces the frontend into error text parsing, one of the most fragile forms of error handling there is.
4. Custom exception classes and error categories
For business specific error cases that go beyond the built in categories, it is worth building a custom exception class with its own extensions.category. The interface Magento\Framework\GraphQl\Exception\GraphQlExceptionInterface defines the method getExtensions(), through which additional structured data can be brought into the error response, for example a machine readable error code or affected field names.
A good custom exception class for GraphQL stays specific enough to be handled in a targeted way in the frontend, but generic enough to be reused across several similar error cases. For a loyalty points module, that could be an InsufficientLoyaltyPointsException that, besides the message, also delivers the currently available point count as an extension, so the frontend can display the correct number directly without an additional query.
<?php
declare(strict_types=1);
namespace Mironsoft\LoyaltyPoints\Model\Exception;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Exception\GraphQlExceptionInterface;
use Magento\Framework\Phrase;
/**
* Thrown when a customer does not have enough loyalty points for redemption.
*/
final class InsufficientLoyaltyPointsException extends LocalizedException implements GraphQlExceptionInterface
{
/**
* @param Phrase $phrase Human-readable error message
* @param int $availablePoints Points currently available to the customer
*/
public function __construct(
Phrase $phrase,
private readonly int $availablePoints
) {
parent::__construct($phrase);
}
/**
* Return structured extension data for the GraphQL error response.
*
* @return array<string, mixed>
*/
public function getExtensions(): array
{
return [
'category' => 'loyalty-insufficient-points',
'available_points' => $this->availablePoints,
];
}
}
5. Partial responses: data and errors at once
One of the strongest properties of GraphQL is the ability to return partial successes: a query with several fields can deliver data for some fields and an error for others, in the same response. An example: a product page query simultaneously requests base data, reviews, and an individual discount from an external system. If the external discount service goes down, base data and reviews should still arrive in data, while only the discount field is null and has a corresponding entry in the errors array.
This partial response capability requires resolvers to handle errors in isolation per field, instead of letting an exception from a single resolver crash the entire query. In practice, that means dependencies on unstable external services should be encapsulated in their own, clearly separated resolvers, so that an outage there does not endanger the rest of the response. Anyone who bundles all data into a single monolithic resolver loses this advantage of GraphQL entirely.
6. Error extensions for structured client logic
Beyond the extensions object, GraphQL errors can carry any additional metadata, as long as it is agreed upon with the frontend team. Commonly useful extensions are a machine readable code for i18n translations in the frontend, a field hint for validation errors in forms, and for rate limiting, a retry_after value in seconds. This structured data lets the frontend react in a targeted way instead of parsing the message text, which can change at any time and is unsuitable for translation.
An important principle for every GraphQL error handling setup: the message is meant for developers and logs, not for direct display in the UI. Translated, user friendly texts should be generated in the frontend based on extensions.code, not by directly displaying the backend error message. This decouples backend language changes from frontend localization and prevents technical error messages from leaking into the UI.
7. Logging and monitoring GraphQL errors
Without systematic logging, GraphQL errors remain invisible. Magento logs unhandled exceptions to var/log/exception.log by default, but deliberately thrown GraphQlInputException errors often do not appear there, because they are treated as expected business errors. For monitoring, it is worth adding a custom plugin on Magento\Framework\GraphQl\Exception\ExceptionFormatter, which additionally forwards every error, structured, to a monitoring system such as Sentry or to a dedicated log channel.
Important for prioritization: not every GraphQL error is equally relevant. A graphql-input error from an incorrect user input is normal and does not need to raise an alert. A repeatedly occurring graphql-no-such-entity error on a specific product, on the other hand, can indicate a data problem in the catalog. A monitoring setup that filters and aggregates by extensions.category delivers much more meaningful signals here than a simple count of all errors.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlMonitoring\Plugin;
use Magento\Framework\GraphQl\Exception\ExceptionFormatter;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Psr\Log\LoggerInterface;
/**
* Plugin that forwards structured GraphQL error data to a dedicated log channel.
*/
final class LogFormattedErrors
{
/**
* @param LoggerInterface $graphQlLogger Dedicated logger channel for GraphQL errors
*/
public function __construct(
private readonly LoggerInterface $graphQlLogger
) {
}
/**
* Log the formatted error alongside its category before returning it unchanged.
*
* @param ExceptionFormatter $subject
* @param array $result
* @return array
*/
public function afterFormat(ExceptionFormatter $subject, array $result): array
{
$this->graphQlLogger->info('GraphQL error', [
'category' => $result['extensions']['category'] ?? 'unknown',
'message' => $result['message'] ?? '',
]);
return $result;
}
}
8. Consuming errors in the frontend
A Hyvä frontend executing GraphQL requests through Alpine.js should generally evaluate both parts of the response: data for successful fields and errors for failed ones. A robust fetch function first checks whether errors is present in the response body, groups the errors by path, and only shows error messages for fields that were actually requested and failed. Fields that successfully delivered data are rendered normally, even though errors exist elsewhere in the same response.
For forms calling a custom GraphQL mutation, it is worth building a mapping table from extensions.category to UI behavior: graphql-input shows an inline validation message on the affected field, graphql-authorization triggers a redirect to login, unknown categories show a generic error message with support contact. This mapping makes error handling in the frontend predictable and maintainable, instead of writing new special case code for every new error scenario.
9. Bad vs. good error handling compared
The following table shows typical patterns of poor GraphQL error handling compared to the recommended approach.
| Aspect | Poor practice | Recommended practice |
|---|---|---|
| Exception type | Throwing generic \Exception | GraphQlInputException and related classes |
| Error categorization | No extensions.category | Consistent categories per error type |
| Partial responses | One field error aborts the entire query | Errors isolated per field, remaining data preserved |
| Frontend display | message text shown directly in the UI | Translated texts generated through extensions.code |
| Monitoring | Only var/log/exception.log, no categorization | Structured logging by category with alerting |
The difference between the two columns determines in practice how quickly a team recognizes production problems and how precisely users are informed about errors. Investment in structured error handling pays off starting with the second or third custom mutation in a project.
Mironsoft
Magento 2 GraphQL development and API architecture
Unclear GraphQL errors in your shop frontend?
We bring structured GraphQL error handling to your Magento shop: custom exception classes, consistent error categories, and monitoring that distinguishes real problems from normal user input.
Exception design
Custom, structured exception classes with meaningful extensions
Frontend integration
Consistent mapping of error categories to UI behavior
Monitoring
Structured error logging with alerting for critical categories
10. Summary
Good GraphQL error handling in Magento 2 starts with understanding that HTTP status 200 says nothing about the success or failure of a query, what matters is the errors array in the response body. The built in exception classes GraphQlInputException, GraphQlAuthorizationException, and GraphQlNoSuchEntityException cover the most common cases consistently, custom exception classes with matching extensions extend the pattern for domain specific errors.
Partial responses allow errors to be handled in isolation per field, instead of letting an entire query fail because of one unstable external service. Structured error extensions let the frontend react in a targeted way without text parsing, and systematic logging by error category makes production problems visible before users report them in large numbers. Together, these building blocks turn GraphQL errors from a blackbox into a traceable, maintainable system.
GraphQL Error Handling in Magento 2 — Key Takeaways
Errors array
HTTP 200 says nothing about success. The errors array next to data holds the actual error information.
Exception classes
GraphQlInputException, GraphQlAuthorizationException, GraphQlNoSuchEntityException for consistent error categories.
Partial responses
Handle errors in isolation per field so a single failure does not endanger the entire response.
Monitoring
Structured logging by extensions.category for meaningful alerting instead of plain error counting.