How do I stop execution in Express.js after catching an error from an Await call?

Viewed 1171

I find lots of posts that almost answer the question, but nothing quite works for me.

I have an async function:

const doStuff = async (data)=>{
    if(data == "a bug")
        throw(new Error('Error! Bug found'));
    return "not a bug";
}

I call the function on a route handler:

app.get('/doStuffRoute', async (req, res, next)=>{
    const result = await doStuff("a bug")
                        .catch((err)=>{return next(err);});

    console.log("This shouldn't execute!");
}

The error gets handled just fine by my custom error middleware, prints out the error, etc. But then I still see This shouldn't execute! being printed as well!

From all my research and reading, await X.catch(error) should be identical to try{ await X } catch {error}. And I really prefer .catch(); if at all possible. There are dozens and dozens of tutorials showing this, and even a few stackoverflow posts explicitly stating this to be the case. However I also occasionally find a post cryptically saying "don't use await with .catch()" with no other explanation or detail, so I don't know what to think.

The only hint I can find is adding return before your next(err) call should be enough to halt execution, but it doesn't seem to actually work if return next(err); is inside a .catch() block.

What is going on? Are my sources lying? Am I misunderstanding some basic fundamental concept? I appreciate any help getting me on track.

As a bonus question, why do so many people suggest a wrapper function a-la express-async-handler? Why would this have any different behavior than just calling the async function directly and handling it?

2 Answers

A rejected promise does not stop further Javascript execution so that's why the .catch() handler doesn't stop anything. In fact, since async functions are asynchronous, your console.log() will even execute before the .catch() handler does. As such, you need to design code flow that respects the promise state.

I would suggest this:

app.get('/doStuffRoute', async (req, res, next) => {
    try {
        const result = await doStuff("a bug");
        console.log("doStuff() resolved");
    } catch(err) {
        console.log("doStuff() rejected");
        next(err);
    }
}

This way, you've clearly delineated the two code paths for a resolved promise and for a rejected promise and you can place code in the appropriate code path.

When you do

const result = await doStuff("a bug").catch((err)=>{return next(err);});
console.log(result) // you will see undefined 

because it is trying to evaluate next method when you pass parameter err and store it in result variable, similarly if you do something like this,

  const add = (err)=>{
    if(err instanceof Error){ return 2}
    return 3
   }
const result = await doStuff("a bug").catch((err)=>{return add(err);});
console.log(result) // you will see 2

Answering your question, when you have a return statement inside a callback function only the callback function's execution ends. which means the main parent function will continue it's execution.

const result = await doStuff("a bug").catch((err)=>{return err;});
console.log(result) // now result is an Error object
if(result instanceof Error){
 next(result) //if an error handler is defined call that or call default express error handler
 return //stop execution of route callback
} 
console.log("will NOT execute if there is an error")

but I would like to add there are better way of handling errors for async express routes, This article explains it in detail.

https://zellwk.com/blog/async-await-express/

Related