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