menu

GraphQL

GraphQL is a query language for APIs, with thousands of tools and libraries built by the community.

Channels
Team

How are you handling errors?

March 28, 2018 at 9:03pm

How are you handling errors?

March 28, 2018 at 9:03pm (Edited 3 years ago)
I'm in the middle of building out a GraphQL API using graphql-js and express.
When building a REST API, it's easy to return an error with a response code, message, and additional info. I'm not sure if this aligns with best practices, but I've been doing it like this:
// in a route, I can do:
res.status(403)
return next({ message: "You don't have permission" })
// and return it to the user in a generic error handler:
app.use((err, req, res, next) => {
if (res.statusCode === 200) res.status(501)
return res.json({ message: err.message || 'Internal server error' })
But with GraphQL, I've just been doing:
throw new Error("You don't have permission")
This is returned to the client as a GraphQLError.
There's a little bit of discussion about this here:
In that thread, one comment mentions:
"
I think of errors in two categories:
  1. Something went wrong and part of a query/operation couldn't be fulfilled.
  2. The user did something wrong and needs to be provided feedback.
"
So, distinguishing between application errors (which we'd forward to something like Sentry) and user errors.I'm feeling a little confused about the best way to handle the latter, because:
  1. Sometimes a mutation (or query) may return one of multiple types of errors. "Field X isn't valid, please fix..." or "You aren't authorized to do this". On the front end, I may want to do different things depending on the error. (In this case, highlight the invalid field, or redirect the user, respectively). The only way I can detect one error vs the other is matching the exact error text: if (err.message === "You aren't ...") , which is clearly not a good idea. (Having an error status code would be better here)
  2. Sometimes multiple errors might be returned. ("Field X isn't valid", "Field Y isn't valid")
  3. Down the line, others might be using the API, and it would be nice to include a little extra information for developers. ("Read more in the docs here")
I suppose I could create a new Error type:
type Error {
messages: [ErrorMessage]
statusCode: Int
readMore: String
}
But, then I'd need to include this as a possible return value on all of the queries and mutations in my schema, as well as in the queries and mutations on the frontend. This seems kind of redundant.
It would also mean that I'd need to look for the errors after every mutation:
this.props.mutate({ variables: formData })
.then((response) => {
if (response.data.errors) {
// ...
}
}).catch((err) => {
// I'd prefer to just catch them here
})
(But, maybe wanting to handle client errors within the catch is a bad practice, since a client error is still technically a "successful" response?)
Anyway -- I'm not necessarily looking for answers for my specific situation. Really, I'm just wondering, how are others thinking about and dealing with this?

March 29, 2018 at 7:31am
We have a custom UserError constructor that we use to denote errors the user should see vs errors the user shouldn't see, and then in formatError (?) we send non-usererrors to sentry.
  • reply
  • like
export const IsUserError = Symbol('IsUserError');
class UserError extends Error {
constructor(...args) {
super(...args);
this.name = 'Error';
this.message = args[0];
this[IsUserError] = true;
Error.captureStackTrace(this, 'Error');
}
}
export default UserError;
like-fill
5
  • reply
  • like
const createGraphQLErrorFormatter = (req?: express$Request) => (
error: GraphQLError
) => {
debug('---GraphQL Error---');
debug(error);
debug('-------------------\n');
const isUserError = error.originalError
? error.originalError[IsUserError]
: error[IsUserError];
let sentryId = 'ID only generated in production';
if (!isUserError) {
if (process.env.NODE_ENV === 'production') {
sentryId = Raven.captureException(
error,
req && Raven.parsers.parseRequest(req)
);
}
}
return {
message: isUserError ? error.message : `Internal server error: ${sentryId}`,
// Hide the stack trace in production mode
stack:
!process.env.NODE_ENV === 'production' ? error.stack.split('\n') : null,
};
};
like-fill
6
  • reply
  • like
I hope that makes sense!
  • reply
  • like

March 30, 2018 at 5:12pm
If you want to show it in the UI, use error types in your schema.
  • reply
  • like
Otherwise you're coupling your UI to schema-less conventions (the shape of the errors array), undermining a lot of the benefits of GraphQL.
like-fill
1
  • reply
  • like
For mutation input, it's pretty easy, you just model it as form errors similar to Django (a form can have an array of field errors, and an array of non-field errors). I'd only do this for complex form-like mutations where the "error" state can be a consequence of user (rather than system) error
  • reply
  • like
For failed permission checks, it comes down to whether it's acceptable for your fields to just return null, or whether you need to know more than that. Assuming you want to communicate that the reason data is missing is that the user doesn't have sufficient permissions, i'd model that as a union between the desired type and some kind of PermissionDenied type. Yes, this *is* more verbose, but precision and correctness tends to be. Forcing developers to consider the failure scenarios is a significant benefit of having a good type system.
like-fill
5
  • reply
  • like

March 31, 2018 at 6:06pm
🙌Amazing, thank you both !!
  • reply
  • like

April 2, 2018 at 5:58pm
Best practices surrounding error handling in GraphQL are still evolving and discussed by the community. I am not aware of that _one_ approach that everyone seems to agree to be the best (yet). There are many trade-offs involved and as with a lot of topics, you have to find the approach that works best for you, given your requirements and constraints. Another resource that can give you some inspiration is this article: https://dev.to/andre/handling-errors-in-graphql--2ea3.
like-fill
3
  • reply
  • like

September 18, 2019 at 8:19pm
What if you do NOT want to send certain errors back to the client? And you just want to send them to something like Sentry. In the example from , we can remove information. Is there a hook/lifecycle where we can decide which one to send and which one to include? Or is our only option to catch the error in the resolver, send to Sentry over there?
  • reply
  • like

September 19, 2019 at 9:44pm
const createGraphQLErrorFormatter = (req?: express$Request) => (
error: GraphQLError
) => {
debug('---GraphQL Error---');
debug(error);
debug('-------------------\n');
const isUserError = error.originalError
? error.originalError[IsUserError]
: error[IsUserError];
let sentryId = 'ID only generated in production';
if (!isUserError) {
if (process.env.NODE_ENV === 'production') {
sentryId = Raven.captureException(
error,
req && Raven.parsers.parseRequest(req)
);
}
}
return {
message: isUserError ? error.message : `Internal server error: ${sentryId}`,
// Hide the stack trace in production mode
stack:
!process.env.NODE_ENV === 'production' ? error.stack.split('\n') : null,
};
};
In case this code is copied from your actual codebase and NodeJS uses the same JS operator precedence as the browser, !process.env.NODE_ENV === 'production' will always be false. You need to use process.env.NODE_ENV !== 'production' or !(process.env.NODE_ENV === 'production)' instead.
Very nice helpers, thank you!
Edited
  • reply
  • like