Search code examples
node.jsexpressasync-awaites6-promiseexpress-4

Express middleware cannot trap errors thrown by async/await, but why?


These two middleware functions behave differently and I cannot figure out why:

Here, the error will get trapped by try/catch:

router.get('/force_async_error/0',  async function (req, res, next) {
  try{
    await Promise.reject(new Error('my zoom 0'));
  }
  catch(err){
    next(err);
  }
});

But here, the error will not get trapped by try/catch:

router.get('/force_async_error/1', async function (req, res, next) {
  await Promise.reject(new Error('my zoom 1'));
});

I thought Express wrapped all middleware functions with try/catch, so I don't see how it would behave differently?

I looked into the Express source, and the handler looks like:

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next); // shouldn't this trap the async/await error?
  } catch (err) {
    next(err);
  }
};

so why doesn't the try/catch there capture the thrown error?


Solution

  • This is because the call is asynchronous, take this code :

    try {
      console.log('Before setTimeout')
      setTimeout(() => {
        throw new Error('Oups')
      })
      console.log('After setTimeout')
    }
    catch(err) {
      console.log('Caught', err)
    }
    console.log("Point of non-return, I can't handle anything anymore")

    If you run it you should see that the error is triggered after Point of non-return. When we're at the throw line it's too late, we're outside of try/catch. At this moment if an error is thrown it'll be uncaught.

    You can work around this by using async/await in the caller (doesn't matter for the callee), ie :

    void async function () {
      try {
        console.log('Before setTimeout')
        await new Promise((resolve, reject) =>
          setTimeout(() => {
            reject(new Error('Oups'))
          })
        )
        console.log('After setTimeout')
      }
      catch(err) {
        console.log('Caught', err.stack)
      }
      console.log("Point of non-return, I can't handle anything anymore")
    }()

    Finally, this means that for Express to handle async errors you would need to change the code to :

    async function handle(req, res, next) {
      // [...]
      try {
        await fn(req, res, next); // shouldn't this trap the async/await error?
      } catch (err) {
        next(err);
      }
    }
    

    A better workaround:

    Define a wrap function like this :

    const wrap = fn => (...args) => Promise
        .resolve(fn(...args))
        .catch(args[2])
    

    And use it like this :

    app.get('/', wrap(async () => {
      await Promise.reject('It crashes!')
    }))