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

4

16

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?

Galagalactagogue answered 21/3, 2018 at 22:30 Comment(3)
I guess it doesn't trap it because the try/catch needs to be within the async function...Galagalactagogue
Great question and good work digging to find the relevant bit of code from the Express source. Just goes to show what a bunch of clowns the Express team (and likely every other team associated with NodeJS) are. "Let's not worry about async functions, no one ever writes those" is probably what they were thinking when they wrote fn(req, res, next) without an await keyword. In Asp.Net, async or not, a top-level catch block catches everything, but in NodeJS, your app is just let to die.Neoma
On top of that, the fact that the NodeJS way of handling this is making you deal with Domains/Clusters or my favourite, making use of packages that monitor your app and force restart it on crash, just goes to show what a s&$t framework it is. This is among the many reasons why I think the web development community in general is a joke.Neoma
P
5

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!')
}))
Polysaccharide answered 21/3, 2018 at 22:50 Comment(6)
yeah, that means that wrapping any async function, you cannot trap the error in there, the try/catch has to go inside the functionGalagalactagogue
It only works if you await the result of the async function, I edited the answer to add a better workaroundPolysaccharide
that wrap function assumes all middleware returns a promise, but o/w yes, it looks like you are correct! thxGalagalactagogue
I was thinking about patching the Layer.prototype.handle_request in Express, just to cause myself problems lolGalagalactagogue
I edited it so you can use promises or concrete values. That's fine, the middlewares doesn't give you enough control for this so it's not that obvious.Polysaccharide
whoever asked the OP must be very dashing indeeGalagalactagogue
H
14

I'm going to add an answer here even though you've already accepted another one because I think what's going on here can be explained better and this will help others attempting to understand this.

In your code here:

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

Let's discuss what is going on:

First, you declared the callback as async which you had to do in order to use await in it. An async function tells the interpreter to do several important things.

1. An async function always returns a promise. The resolved value of the promise will be whatever the function returns.

2. An async function is internally wrapped with a try/catch. If any exceptions are thrown in the top level scope of the function code, then those exceptions will be caught and will automatically reject the promise that the function returns.

3. An async function allows you to use await. This is an indicator to the interpreter that it should implement and allow the await syntax inside the function. This is tied to the previous two points above which is why you can't use await in just any 'ol function. Any uncaught rejections from await will also reject the promise that the function returns.

It's important to understand that while the async/await syntax allows you to kind of program with exceptions and try/catch like synchronous code, it isn't exactly the same thing. The function is still returning a promise immediately and uncaught exceptions in the function cause that promise to get rejected at some time later. They don't cause a synchronous exception to bubble up to the caller. So, the Express try/catch won't see a synchronous exception.

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

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

so why doesn't the try/catch [in Express] there capture the thrown error?

This is for two reasons:

  1. The rejected promise is not a synchronous throw so there's no way for Express to catch it with a try/catch. The function just returns a rejected promise.

  2. Express is not looking at the return value of the route handler callback at all (you can see that in the Express code you show). So, the fact that your async function returns a promise which is later rejected is just completely ignored by Express. It just does this fn(req, res, next); and does not pay attention to the returned promise. Thus the rejection of the promise falls on deaf ears.

There is a somewhat Express-like framework called Koa that uses promises a lot and does pay attention to returned promises and which would see your rejected promise. But, that's not what Express does.


If you wanted some Koa-type functionality in Express, you could implement it yourself. In order to leave other functionality undisturbed so it can work normally, I'll implement a new method called getAsync that does use promises:

router.getAsync = function(...args) {
    let fn = args.pop();
    // replace route with our own route wrapper
    args.push(function(req, res, next) {
        let p = fn(req, res, next);
        // if it looks like a promise was returned here
        if (p && typeof p.catch === "function") {
            p.catch(err => {
                next(err);
            });
        }
    });
    return router.get(...args);
}

You could then do this:

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

And, it would properly call next(err) with your error.

Or, your code could even just be this:

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

P.S. In a full implementation, you'd probably make async versions of a bunch of the verbs and you'd implement it for middleware and you'd put it on the router prototype. But, this example is to show you how that could work, not to do a full implementation here.

Hermilahermina answered 22/3, 2018 at 1:5 Comment(3)
upvoted, nice answer, thanks, hopefully will help a few pplGalagalactagogue
@Olegzandr - Added some code that would make your code example work.Hermilahermina
whoever asked the OP must be very dashing indeedGalagalactagogue
P
5

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!')
}))
Polysaccharide answered 21/3, 2018 at 22:50 Comment(6)
yeah, that means that wrapping any async function, you cannot trap the error in there, the try/catch has to go inside the functionGalagalactagogue
It only works if you await the result of the async function, I edited the answer to add a better workaroundPolysaccharide
that wrap function assumes all middleware returns a promise, but o/w yes, it looks like you are correct! thxGalagalactagogue
I was thinking about patching the Layer.prototype.handle_request in Express, just to cause myself problems lolGalagalactagogue
I edited it so you can use promises or concrete values. That's fine, the middlewares doesn't give you enough control for this so it's not that obvious.Polysaccharide
whoever asked the OP must be very dashing indeeGalagalactagogue
C
0

Neither of these really answer the question, which if I understand correctly is:

Since the async/await syntax lets you handle rejected "awaits" with non-async style try/catch syntax, why doesn't a failed "await" get handled by Express' try/catch at the top level and turned into a 500 for you?

I believe the answer is that whatever function in the Express internals that calls you would also have to be declared with "async" and invoke your handler with "await" to enable async-catching try/catch to work at that level.

Wonder if there's a feature request for the Express team? All they'd need to add is two keywords in two places. If success, do nothing, if exception hand off to the error handling stack.

Christiansen answered 19/12, 2018 at 1:2 Comment(0)
C
0

Beware that if you don't await or return the promise, it has nothing to do with express - it just crashes the whole process.

For a general solution for detached promise rejections: https://stackoverflow.com/a/28709667

Copied from above answer:

process.on("unhandledRejection", function(reason, p){
    console.log("Unhandled", reason, p); // log all your errors, "unsuppressing" them.
    //throw reason; // optional, in case you want to treat these as errors
}); 
Cheshire answered 27/9, 2022 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.