Error Handling
Errors within the GraphQL engine can be thought of as falling into one of three groups: schema errors,
input errors and processing errors. For instance, if an invalid query was passed to the DocumentExecuter,
it would be considered an input error, and a SyntaxError would be thrown. Or if an invalid enum string
was passed as a variable to a query, an InvalidValueError would be thrown. All validation rules that
fail their respective tests are treated as input errors. Processing errors typically represent unanticipated
exceptions occurring during the execution of a field resolver, such as a timeout during a database operation.
Input errors and processing errors are returned from the DocumentExecuter within the ExecutionResult.Errors
property as a list of ExecutionError objects. ExecutionError is derived from Exception, and the Message
property is serialized according to the spec
with location and path information. In addition, by default two additional pieces of information are serialized
to the extensions property of the GraphQL error which contain:
- Within
code, theCodeproperty of theExecutionError, if any, and - Within
codes, theCodeproperty of theExecutionErroralong with generated codes of any inner exceptions, if any.
Note that the data property is not included in the error extensions by default. To include it, you must
configure the ErrorInfoProvider to set ExposeData = true (see Error Serialization below).
Note that by default, messages from unhandled processing errors are masked and a generic "Error trying to resolve field '<FIELD_NAME>'." or similar error is returned.
Here is a sample result of a FormatException thrown within a product field resolver (a processing error):
{
"errors": [
{
"message": "Error trying to resolve field 'product'.",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": [
"product"
],
"extensions": {
"code": "FORMAT",
"codes": [
"FORMAT"
]
}
}
]
}Schema Errors
Schema errors throw an exception during the process of defining or building the schema. For instance,
adding a two fields of the same name to a GraphQL type would result in an ArgumentOutOfRangeException
while attempting to add the second field. Another example would be if a schema defined an invalid union;
an error would be thrown while the schema was being initialized within DocumentExecuter and caught as
an unhandled exception (see Processing Errors below).
Input Errors
All input errors generated by GraphQL.NET derive from DocumentError. Below is a list of error
messages and their respective error classes and codes:
| Description | Error class | Code |
|---|---|---|
| Empty query document | NoOperationError | NO_OPERATION |
| Query parsing error | SyntaxError | SYNTAX_ERROR |
| Attempting a mutation or subscription when none are defined | InvalidOperationError | INVALID_OPERATION |
| Invalid variable values | InvalidVariableError | INVALID_VALUE |
| Validation errors | derived from ValidationError | (varies; see list below) |
Field resolvers can manually trigger an input error by throwing an ExecutionError or derived class.
Any other thrown error is treated as a processing error (see Processing Errors below).
Here is an example of typical validation within a field resolver that returns an input error:
Field<NonNullGraphType<OrderGraph>>("order")
.Argument<NonNullGraphType<IntGraphType>>("id")
.Resolve(context =>
{
var order = _orderService.GetById(context.GetArgument<int>("id"));
if (order == null)
throw new ExecutionError("Invalid order id");
});You can also add errors to the IResolveFieldContext.Errors property directly.
Field<DroidType>("hero")
.Resolve(context =>
{
context.Errors.Add(new ExecutionError("Error Message"));
return ...;
});
);Processing Errors
Processing errors should only occur if an exception is thrown from within a field resolver.
For instance, if you execute .Single() on an empty array, causing an InvalidOperationException
to be thrown. These types of errors are most likely to be bugs or connection problems, such as a
connection error when communicating to a database. There are also two other types of processing
errors to be aware of:
- Calling
context.GetArgument<>with a type that does not match the argument type, when the system
cannot perform the conversion – for instance, calling context.GetArgument<Guid>("arg") on an argument
of type IntGraphType, and
- Returning data from a field resolver that does not match the graph type of field resolver, when the
system cannot perform the conversion.
Processing errors can be thrown back to the caller of DocumentExecuter.ExecuteAsync by setting the
ExecutionOptions.ThrowOnUnhandledException property to true. When this property is set to false,
the default setting, unhandled exceptions are wrapped in an UnhandledError and added with a generic
error message to the ExecutionResult.Errors property. Error codes are dynamically generated from the
inner exceptions of the wrapped exception and also returned. Data contained within the inner exception's
Data property can also be included in the error response by configuring the error serialization options
(see Error Serialization below).
You can also handle these processing exceptions by setting a delegate within the
ExecutionOptions.UnhandledExceptionDelegate property. Within the delegate you can log the error message
and stack trace for debugging needs. You can also override the generic error message with a more specific
message, wrap or replace the exception with your own ExecutionError class, and/or set the codes and data
as necessary. Note that if ThrowOnUnhandledException is true, the UnhandledExceptionDelegate will not be called.
Here is a sample of a typical unhandled exception delegate which logs the error to a database. It also returns the log id along with the error message:
var executer = new DocumentExecuter();
var result = executer.ExecuteAsync(options =>
{
...
options.UnhandledExceptionDelegate = async context =>
{
try
{
using var db = new MyDatabaseContext();
var errorLog = new ErrorLog {
DateStamp = DateTime.UtcNow,
Message = context.Exception.Message,
Details = context.Exception.ToString()
};
db.ErrorLogs.Add(errorLog);
await db.SaveChangesAsync();
context.Exception.Data["errorLogId"] = errorLog.Id;
}
catch
{
}
};
});You can also override the serialized exception by setting context.Exception, or simply replace
the message by setting context.ErrorMessage such as in this example:
options.UnhandledExecutionDelegate = ctx =>
{
if (ctx.Exception is SqlException)
ctx.ErrorMessage = "A database error has occurred.";
};When using the IGraphQLBuilder to configure your execution, you can use the AddUnhandledExceptionHandler
method to register a delegate to handle unhandled exceptions, as shown in the example below:
services.AddGraphQL(b => b
.AddSchema<MySchema()
.AddUnhandledExceptionHandler(async (context, options) =>
{
try
{
// create dedicated scope to be sure database changes in the parent scope
// are not committed to the database
await using var scope = options.RequestServices!.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<MyDatabaseContext>();
var errorLog = new ErrorLog {
// when APQ is in use, pull the query from the document if necessary
Query = options.Query ?? options.Document?.Source.ToString(),
DateStamp = DateTime.UtcNow,
Message = context.Exception.Message,
Details = context.Exception.ToString()
};
db.ErrorLogs.Add(errorLog);
await db.SaveChangesAsync();
context.Exception.Data["errorLogId"] = errorLog.Id;
}
catch
{
}
})
);Please note that the unhandled exception handler is not called when the request's cancellation token is triggered, for instance when the client disconnects before execution is complete.
Error Serialization
After the DocumentExecuter has returned a ExecutionResult containing the data and/or errors,
typically you will pass this object to an implementation of IGraphQLSerializer to convert the
object tree into json. The IGraphQLSerializer implementations provided by the GraphQL.SystemTextJson
and GraphQL.NewtonsoftJson packages allow you to configure error serialization by providing an
IErrorInfoProvider implementation. If you are using a dependency injection framework, you can register
the IErrorInfoProvider instance and it will be consumed by the IGraphQLSerializer implementation
automatically. Please review the serialization documentation for more details.
Configuring Error Serialization Options
The default ErrorInfoProvider implementation provides several options to control what information is
included in serialized errors. These options are configured via the ErrorInfoProviderOptions class:
Available Options
| Option | Default | Description |
|---|---|---|
ExposeExtensions |
true |
When true, the extensions property is included in errors. When false, no extensions are serialized regardless of other settings. |
ExposeCode |
true |
When true and ExposeExtensions is true, includes the error's Code property in extensions.code. |
ExposeCodes |
true |
When true and ExposeExtensions is true, includes the error's code and all inner exception codes in extensions.codes as an array. |
ExposeData |
false |
When true and ExposeExtensions is true, includes the contents of ExecutionError.Data in extensions.data. Note: This is disabled by default for security reasons. |
ExposeExceptionDetails |
false |
When true, exposes detailed exception information including stack traces. Note: This should only be enabled in development environments. |
ExposeExceptionDetailsMode |
Extensions |
Controls where exception details are placed when ExposeExceptionDetails is true: Message places details in the error message, Extensions places details in extensions.details. |
Configuring ErrorInfoProvider
You can configure the ErrorInfoProvider in several ways:
Using dependency injection with IGraphQLBuilder:
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddErrorInfoProvider(opt => {
opt.ExposeData = true; // Enable data property
opt.ExposeExceptionDetails = true; // Only in development!
})
);Creating an instance directly:
var errorInfoProvider = new ErrorInfoProvider(options =>
{
options.ExposeData = true;
options.ExposeExceptionDetails = true;
options.ExposeExceptionDetailsMode = ExposeExceptionDetailsMode.Extensions;
});
var serializer = new GraphQLSerializer(errorInfoProvider);Example Error Outputs
Default configuration (no data, no details):
{
"errors": [
{
"message": "Error trying to resolve field 'product'.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["product"],
"extensions": {
"code": "FORMAT",
"codes": ["FORMAT"]
}
}
]
}With ExposeData enabled:
{
"errors": [
{
"message": "Error trying to resolve field 'product'.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["product"],
"extensions": {
"code": "FORMAT",
"codes": ["FORMAT"],
"data": {
"customKey": "customValue"
}
}
}
]
}With ExposeExceptionDetails enabled (ExposeExceptionDetailsMode = Extensions):
{
"errors": [
{
"message": "Error trying to resolve field 'product'.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["product"],
"extensions": {
"code": "FORMAT",
"codes": ["FORMAT"],
"details": "System.FormatException: Input string was not in a correct format.\n at ..."
}
}
]
}With ExposeExceptionDetails enabled (ExposeExceptionDetailsMode = Message):
{
"errors": [
{
"message": "System.FormatException: Input string was not in a correct format.\n at ...",
"locations": [{ "line": 3, "column": 5 }],
"path": ["product"],
"extensions": {
"code": "FORMAT",
"codes": ["FORMAT"]
}
}
]
}Security Warning: Never enable
ExposeExceptionDetailsin production environments as it can expose sensitive information such as file paths, connection strings, and internal implementation details.
Validation error reference list
Here is a full list of validation errors produced by GraphQL.NET:
| Rule | Code | Number |
|---|---|---|
| UniqueOperationNames | UNIQUE_OPERATION_NAMES |
5.2.1.1 |
| LoneAnonymousOperation | LONE_ANONYMOUS_OPERATION |
5.2.2.1 |
| SingleRootFieldSubscriptions | SINGLE_ROOT_FIELD_SUBSCRIPTIONS |
5.2.3.1 |
| FieldsOnCorrectType | FIELDS_ON_CORRECT_TYPE |
5.3.1 |
| OverlappingFieldsCanBeMerged | OVERLAPPING_FIELDS_CAN_BE_MERGED |
5.3.2 |
| ScalarLeafs | SCALAR_LEAFS |
5.3.3 |
| KnownArgumentNames | KNOWN_ARGUMENT_NAMES |
5.4.1 |
| UniqueArgumentNames | UNIQUE_ARGUMENT_NAMES |
5.4.2 |
| ProvidedNonNullArguments | PROVIDED_NON_NULL_ARGUMENTS |
5.4.2.1 |
| UniqueFragmentNames | UNIQUE_FRAGMENT_NAMES |
5.5.1.1 |
| KnownTypeNames | KNOWN_TYPE_NAMES |
5.5.1.2 |
| FragmentsOnCompositeTypes | FRAGMENTS_ON_COMPOSITE_TYPES |
5.5.1.3 |
| NoUnusedFragments | NO_UNUSED_FRAGMENTS |
5.5.1.4 |
| KnownFragmentNames | KNOWN_FRAGMENT_NAMES |
5.5.2.1 |
| NoFragmentCycles | NO_FRAGMENT_CYCLES |
5.5.2.2 |
| PossibleFragmentSpreads | POSSIBLE_FRAGMENT_SPREADS |
5.5.2.3 |
| ArgumentsOfCorrectType | ARGUMENTS_OF_CORRECT_TYPE |
5.6.1 |
| DefaultValuesOfCorrectType | DEFAULT_VALUES_OF_CORRECT_TYPE |
5.6.1 |
| UniqueInputFieldNames | UNIQUE_INPUT_FIELD_NAMES |
5.6.3 |
| KnownDirectivesInAllowedLocations | KNOWN_DIRECTIVES |
5.7.1 |
| KnownDirectivesInAllowedLocations | DIRECTIVES_IN_ALLOWED_LOCATIONS |
5.7.2 |
| UniqueDirectivesPerLocation | UNIQUE_DIRECTIVES_PER_LOCATION |
5.7.3 |
| UniqueVariableNames | UNIQUE_VARIABLE_NAMES |
5.8.1 |
| VariablesAreInputTypes | VARIABLES_ARE_INPUT_TYPES |
5.8.2 |
| NoUndefinedVariables | NO_UNDEFINED_VARIABLES |
5.8.3 |
| NoUnusedVariables | NO_UNUSED_VARIABLES |
5.8.4 |
| VariablesInAllowedPosition | VARIABLES_IN_ALLOWED_POSITION |
5.8.5 |