Another GraphQL Error Handling Alternative

Note Statistics

Note Statistics

  • Viewed 230 times
Thu, 11/11/2021 - 21:17

This gist offers an alternative or complementary solution to the default GraphQL error handling. So what is wrong with the usual error handling solution provided by the GraphQL specification?

1. No schema for errors types: When an error occurs, the errors field in the response is populated. However, the client knows nothing about what might be found errors array. How do you distinguish between "duplicate user name" and "password is invalid' error. The extensions key allows providers to add extra fields but, and they need to be documented outside of the schema. Not ideal.

2. Null field when an error occurs: The corresponding field should be null. This can potentially not ideal if you're wanting to have errors returned as part of a mutation, but want to query for data on the result anyways.

3. Exceptional Only: Errors are today generally accepted as a way to represent exception. What about user errors?

4. Determine error source: Having the error located somewhere different by default does not really help developers to effectively handle errors. You have a path array that describes where an error occurs (e.g. [ user, password ]). You can build some custom function in your client that maps an error to your query path, but it is not ideal.

The issues mentioned above are mentioned in dept in the following articles

This Gist only aims to complement the mentioned article by suggesting another possible option.

Here is a sample schema

type Mutation {
  createUser(input: CreateUserInput): CreateUserResult
}

union CreateUserResult = UserCreated | CreateUserFailed

type UserCreated {
  user: User!
}

type CreateUserFailed {
  userErrors: [CreateUserError!]!
}

union CreateUserError = UserNameTaken | PasswordTooShort | MustBeOver18

type UserNameTaken implements UserError {
  message: String!
  suggestion: String!
}

interface UserError {
  # A description of the error
  message: String!
  # A path to the input value that caused the error
  path: [String!]
}

and a sample query

mutation {
  createUser(input: {}) {
    # Success: Interface contract
    ... on UserCreated {
      user {
        id
      }
    }

    ... on CreateUserFailed {
      # Error: Specific cases
      ... on UserNameTaken {
        message
        path
        suggestion
      }
      # Error: Interface contract
      ... on UserError {
        message
        path
      }
    }
  }
}

in your resolver

const resolvers = {
  Muataion: {
    createUser: async (parent, args, context) => {
      const createUserResult = await context.db.createuser(args.input);
      if (createUserResult.ok) {
        return {
          __typename: "UserCreated",
          user: createUserResult.user,
        };
      }
      return  {
			  __typename: "CreateUserFailed"
				userErrors: createUserResult.errors.map(error => ({__typename:typeof error , ...error}))
			};
    },
  },
};

Pros:

  • Expressive and Discoverable Schema
  • Support for Multiple Errors
  • Easier Evolution

Cons:

  • Quite Verbose
Authored by