Ad

How To Handle Async Errors Correctly?

When making a GraphQL query, and the query fails, Apollo solves this by having a data-object and an error-object.

When an async error is happening, we get the same functionality with one data-object and one error-object. But, this time we get an UnhandledPromiseRejectionWarning too, with information about: DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code..

So, we obviously need to solve this, but we want our async-functions to cast errors all the way up to Apollo. Do we need to try...catch all functions and just pass our error further up the tree? Coming from C#, were an exception just goes all the way to the top if never caught, it sounds like a tedious job to tell Apollo GraphQL that one (or more) leaves failed to retrieve data from the database.

Is there a better way to solve this, or is there any way to tell javascript/node that an uncaught error should be passed further up the call tree, until it's caught?

Ad

Answer

If you correctly chain your promises, you should never see this warning and all of your errors will be caught by GraphQL. Assume we have these two functions that return a Promise, the latter of which always rejects:

async function doSomething() {
  return
}

async function alwaysReject() {
  return Promise.reject(new Error('Oh no!'))
}

First, some correct examples:

someField: async () => {
  await alwaysReject()
  await doSomething()
},

// Or without async/await syntax
someField: () => {
  return alwaysReject()
    .then(() => {
      return doSomething()
    })
  // or...
  return alwaysReject().then(doSomething)
},

In all of these cases, you'll see the error inside the errors array and no warning in your console. We could reverse the order of the functions (calling doSomething first) and this would still be the case.

Now, let's break our code:

someField: async () => {
  alwaysReject()
  await doSomething()
},

someField: () => {
  alwaysReject() // <-- Note the missing return
    .then(() => {
      return doSomething()
    })
},

In these examples, we're firing off the function, but we're not awaiting the returned Promise. That means execution of our resolver continues. If the unawaited Promise resolves, there's nothing we can do with its result -- if it rejects, there's nothing we can do about the error (it's unhandled, as the warning indicates).

In general, you should always ensure your Promises are chained correctly as shown above. This is significantly easier to do with async/await syntax, since it's exceptionally easy to miss a return without it.

What about side effects?

There may be functions that return a Promise that you want to run, but don't want to pause your resolver's execution for. Whether the Promise resolves or returns is irrelevant to what your resolver returns, you just need it to run. In these cases, we just need a catch to handle the promise being rejected:

someField: async () => {
  alwaysReject()
    .catch((error) => {
      // Do something with the error
    })
  await doSomething()
},

Here, we call alwaysReject and execution continues onto doSomething. If alwaysReject eventually rejects, the error will be caught and no warning will be shown in the console.

Note: These "side effects" are not awaited, meaning GraphQL execution will continue and could very well finish while they are still running. There's no way to include errors from side effects inside your GraphQL response (i.e. the errors array), at best you can just log them. If you want a particular Promise's rejection reason to show up in the response, you need to await it inside your resolver instead of treating it like a side effect.

A final word on try/catch and catch

When dealing with Promises, we often see errors caught after our function call, for example:

try {
  await doSomething()
} catch (error) {
  // handle error
}

return doSomething.catch((error) => {
  //handle error
})

This is important inside a synchronous context (for example, when building a REST api with express). Failing to catch rejected promises will result in the familiar UnhandledPromiseRejectionWarning. However, because GraphQL's execution layer effectively functions as one giant try/catch, it's not really necessary to catch your errors as long as your Promises are chained/awaited properly. This is true unless A) you're dealing with side effects as already illustrated, or B) you want to prevent the error from bubbling up:

try {
  // execution halts because we await
  await alwaysReject()
catch (error) {
  // error is caught, so execution will continue (unless I throw the error)
  // because the resolver itself doesn't reject, the error won't be bubbled up
}
await doSomething()
Ad
source: stackoverflow.com
Ad