Node.js error-handling gotcha: synchronous errors from an asynchronous function
Tags: javascript, node
Published: 2021-11-07 16:59:38

One thing to look out for in Node.js: functions that may throw an error either synchronously or asynchronously (which, technically, is any function that does async work). You may think you've got pretty good error-handling, but they complicate things.

For instance, here's a basic example:

```js
function getAdditionalData(input) {
  const url = getRequestUrl(input);
  
  return makeHttprequest(url).then(response => {
    return response.data;
  });
}

app.get('/data', (req, res) => {
  const input = calculateInput();
  return getAdditionalData(input).then(result => {
    return { input, result };
  });
});
```

This is a simple endpoint that calls `getAdditionalData()` to get some data and return to the user. The function does an asynchronous thing (HTTP request), so it returns a promise.

Let's think about error-handling. If something goes wrong (maybe the APi is unavailable), we don't want our whole app to crash. Instead, we'd rather return the incomplete data to the user. The obvious way to achieve this is to add a `.catch()`:


```js
app.get('/data', (req, res) => {
  const input = calculateInput();
  return getAdditionalData(input).then(result => {
    return { input, result };
  }).catch(e => {
    // Report error somehow
    console.log("An error occurred", e);
    // But return the original instead of crashing
    return { input };
  });
});
```

Most folks would stop here. But the hidden problem is that `getAdditionalData()` may throw a synchronous error, and that will not be caught by `promise.catch()`. For example, if the function `getRequestUrl()` does not exist, then a `ReferenceError` will be thrown. And **it will not be passed to our `.catch`**. This is because Promise rejections (what `.catch` gets) and exceptions are two independent error-handling mechanisms in Node.js. This exception will not be handled, and will either cause the process to crash, or be caught by your global `process.on('uncaughtException')` handler.

How do we fix this? There are two ways. Option one: switch to async/await, which converts Promise rejections to thrown exceptions, so you can handle both with try/catch.

```js
app.get('/data', async (req, res) => {
  const input = calculateInput();
  try {
    const result = await getAdditionalData(input);
    return { input, result };
  } catch(e) {
    console.log("An error occurred", e);
    return { input };
  }
});
```

However, try/catch can disrupt the flow of your code sometimes, and you might want to stick with raw promises. In that case, you can use the other option: make sure `getAdditionalData()` only rejects, instead of throws. You can do this by marking it as `async` (even if you aren't using `await` inside it):


```diff
-function getAdditionalData(input) {
+async function getAdditionalData(input) {
+ // Now, thrown errors will be converted to rejections
  // ...
}

app.get('/data', (req, res) => {
  const input = calculateInput();
  return getAdditionalData(input).then(result => {
    return { input, result };
  }).catch(e => {
    console.log("An error occurred", e);
    return { input };
  });
});
```

Of course, you can only do this if you can modify the function's source code. Otherwise, you'd have to do something like this:

```js
function getAdditionalData(input) {
  // ...
}

app.get('/data', (req, res) => {
  const input = calculateInput();
  return new Promise((resolve, reject) => {
    try {
      getAdditionalData(input).then(resolve);
    } catch (e) {
      reject(e);
    }
  }).catch(e => {
    console.log("An error occurred", e);
    return { input };
  });
});
```

...in which case, you're back to try/catch, so you might as well use the first method.

I guess the moral of this story is: _if your function returns a promise, it might be a good idea to mark it as `async`, so its errors are always turned into rejections_.

Error handling in Node.js can be a real minefield, what with synchronous code, promises, callbacks, and event emitters. Unexpected scenarios can arise when they interact, like when an error is thrown inside a timer, or an error is thrown in an `error` event handler. James Snell has [an example](https://twitter.com/jasnell/status/1271165910154227713) of this, and his ["Broken Promises" talk](https://www.youtube.com/watch?v=XV-u_Ow47s0) goes into some depth on some gotchas around working with promises.