v0.17.97
v0.17.97 v0.17.96 v0.17.95 v0.17.94 v0.17.93 v0.17.92 v0.17.91 v0.17.90 v0.17.89 v0.17.88 v0.17.87 v0.17.86 v0.17.85 v0.17.84 v0.17.83 v0.17.82 v0.17.81 v0.17.80 v0.17.79 v0.17.78 master

Handling Errors

Sending custom error data in the graphql response
[edit]

Returning errors

All resolvers simply return an error to be sent to the user. The assumption is that any error message returned here is appropriate for end users. If certain messages aren’t safe, customise the error presenter.

Multiple errors

To return multiple errors you can call the graphql.Error functions like so:

package foo

import (
	"context"
	"errors"

	"github.com/vektah/gqlparser/v2/gqlerror"
	"github.com/99designs/gqlgen/graphql"
)

// DoThings add errors to the stack.
func (r Query) DoThings(ctx context.Context) (bool, error) {
	// Print a formatted string
	graphql.AddErrorf(ctx, "Error %d", 1)

	// Pass an existing error out
	graphql.AddError(ctx, gqlerror.Errorf("zzzzzt"))

	// Or fully customize the error
	graphql.AddError(ctx, &gqlerror.Error{
		Path:       graphql.GetPath(ctx),
		Message:    "A descriptive error message",
		Extensions: map[string]interface{}{
			"code": "10-4",
		},
	})

	// And you can still return an error if you need
	return false, gqlerror.Errorf("BOOM! Headshot")
}

They will be returned in the same order in the response, eg:

{
  "data": {
    "todo": null
  },
  "errors": [
    { "message": "Error 1", "path": [ "todo" ] },
    { "message": "zzzzzt", "path": [ "todo" ] },
    { "message": "A descriptive error message", "path": [ "todo" ], "extensions": { "code": "10-4" } },
    { "message": "BOOM! Headshot", "path": [ "todo" ] }
  ]
}

or you can simply return multiple errors

package foo

import (
	"context"
	"errors"

	"github.com/vektah/gqlparser/v2/gqlerror"
	"github.com/99designs/gqlgen/graphql"
)

var errSomethingWrong = errors.New("some validation failed")

// DoThingsReturnMultipleErrors collect errors and returns it if any.
func (r Query) DoThingsReturnMultipleErrors(ctx context.Context) (bool, error) {
	errList := gqlerror.List{}

	// Add existing error
	errList = append(errList, gqlerror.Wrap(errSomethingWrong))

	// Create new formatted and append
	errList = append(errList, gqlerror.Errorf("invalid value: %s", "invalid"))

	// Or fully customize the error and append
	errList = append(errList, &gqlerror.Error{
		Path:       graphql.GetPath(ctx),
		Message:    "A descriptive error message",
		Extensions: map[string]interface{}{
			"code": "10-4",
		},
	})

	return false, errList
}

They will be returned in the same order in the response, eg:

{
  "data": {
    "todo": null
  },
  "errors": [
    { "message": "some validation failed", "path": [ "todo" ] },
    { "message": "invalid value: invalid", "path": [ "todo" ] },
    { "message": "A descriptive error message", "path": [ "todo" ], "extensions": { "code": "10-4" } },
  ]
}

Returning errors from extensions

When writing a HandlerExtension (such as for rate limiting or authentication), you might want to short-circuit the request and return an error without executing the query.

When doing so, you must wrap your error response using graphql.OneShot. Since streaming transports like WebSockets or Server-Sent Events expect a stream of responses ending in nil, returning a bare closure that always yields the error will cause the transport to loop infinitely and spam the client, or cause high CPU consumption and flood logs.

package foo

import (
	"context"

	"github.com/99designs/gqlgen/graphql"
	"github.com/vektah/gqlparser/v2/gqlerror"
)

type MyInterceptor struct{}

func (m MyInterceptor) ExtensionName() string {
	return "MyInterceptor"
}

func (m MyInterceptor) Validate(schema graphql.ExecutableSchema) error {
	return nil
}

func (m MyInterceptor) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
	if !isAuthorized(ctx) {
		err := gqlerror.Errorf("unauthorized")
		// ALWAYS use graphql.OneShot when short-circuiting a request!
		return graphql.OneShot(graphql.ErrorResponse(ctx, err.Message))
	}
	return next(ctx)
}

func isAuthorized(ctx context.Context) bool {
    return false // implement authorization logic here
}

Hooks

The error presenter

All errors returned by resolvers, or from validation, pass through a hook before being displayed to the user. This hook gives you the ability to customise errors however makes sense in your app.

The default error presenter will capture the resolver path and use the Error() message in the response.

You change this when creating the server:

package bar

import (
	"context"
	"errors"

	"github.com/vektah/gqlparser/v2/gqlerror"
	"github.com/99designs/gqlgen/graphql"
	"github.com/99designs/gqlgen/graphql/handler"
)

func main() {
	server := handler.New(MakeExecutableSchema(resolvers))

	// Server setup...

	server.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
		err := graphql.DefaultErrorPresenter(ctx, e)

		var myErr *MyError
		if errors.As(e, &myErr) {
			err.Message = "Eeek!"
		}

		return err
	})
}

This function will be called with the same resolver context that generated it, so you can extract the current resolver path and whatever other state you might want to notify the client about.

Non-null violations

When a field the schema declares non-null (String!) resolves to null, gqlgen raises a field error and propagates the null to the nearest nullable ancestor, as the GraphQL specification requires. The error it raises is a *graphql.InvalidNullError, which carries the diagnosis as fields rather than only as message text:

server.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
	err := graphql.DefaultErrorPresenter(ctx, e)

	if nullErr, ok := errors.AsType[*graphql.InvalidNullError](e); ok {
		err.Extensions = map[string]any{
			"code":  "INVALID_NULL",
			"field": nullErr.SchemaCoordinate(), // "OuterObject.inner"
			"type":  nullErr.DeclaredType,       // "InnerObject!"
			"from":  string(nullErr.Source),     // "model_field"
		}
	}

	return err
})

errors.Is(e, graphql.ErrInvalidNull) matches any such violation when you only need to know that one happened.

SchemaCoordinate() returns the field’s schema coordinate. It names the concrete object type, which the error path cannot: for a field reached through an interface the path shows ["node","child"], while the coordinate shows ConcreteNodeA.child. Source says where to look in your Go code:

Source Meaning
resolver A resolver method returned nil.
model_method A method on the model returned nil.
model_field A model struct field was nil, and the field has no resolver.
marshaler A marshaler mapped a non-nil value to null (MarshalTime on the zero time).
runtime_non_null graphql.MarkNonNull raised a nullable field that then resolved to nil.

ListElement reports that the null was an element of a list rather than the field’s own value; Path is the response path, including list indices.

The text of Error() is intended for developers and is not part of the API — match on the type or the sentinel rather than on the message.

Structured logging

InvalidNullError also answers Attrs() []slog.Attr, so logging middleware can record the diagnosis without importing gqlgen’s type. In a response interceptor, collect each error’s attributes from resp := next(ctx) with gqlerror.CollectAttrs:

for _, err := range resp.Errors {
	slog.LogAttrs(ctx, slog.LevelError, "graphql error", gqlerror.CollectAttrs(err)...)
}

A violation reports one gql group holding field, type, null_source, path, and list_element when it applies; empty values are omitted. CollectAttrs merges it with the wrapping *gqlerror.Error’s own gql group, where the outer path wins. Since the first value for each key wins, pass CollectAttrs one error at a time rather than a whole gqlerror.List, which would fold every error into one group.

The panic handler

There is also a panic handler, called whenever a panic happens to gracefully return a message to the user before stopping parsing. This is a good spot to notify your bug tracker and send a custom message to the user. Any errors returned from here will also go through the error presenter.

You change this when creating the server:

server := handler.New(MakeExecutableSchema(resolvers))

// Server setup...

server.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
    // Notify bug tracker...

		return gqlerror.Errorf("Internal server error!")
})

While these handlers are useful in production to make sure the program does not crash, even if a user finds an issue that causes a crash-condition. During development, it can sometimes be more useful to properly crash, potentially generating a coredump to enable further debugging.

To allow your program to crash on a panic, add this to your config file:

omit_panic_handler: true