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, the Code property of the ExecutionError, if any,
  • Within codes, the Code property of the ExecutionError along with generated codes of any inner exceptions, if any, and
  • Within data, the contents of the ExecutionError.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