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 three additional pieces of information are serialized
to the extensions
property of the GraphQL error which contain:
- Within
code
, theCode
property of theExecutionError
, if any, - Within
codes
, theCode
property of theExecutionError
along with generated codes of any inner exceptions, if any, and - Within
data
, the contents of theExecutionError.Data
property, which by default contains the data of the inner exception, if any.
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 along with data contained within the inner
exception's Data
property.
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.
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 |