The hard error handling case made easy with async/await
Let me show a surprisingly trivial looking async/await code which is actually hard to implement correctly with Promises.
async function(request, response, next) {
try {
await validate();
} catch (error) {
return response.status(400).send(error);
}
try {
const result = await doRealWork();
return response.send(result);
} catch (error) {
return next(error);
}
}
Looks simple enough, let’s try to convert this back to use Promises.
function(request, response, next) {
return validate()
.catch((error) => response.status(400).send(error))
.then(() => doRealWork())
.then((result) => response.send(result))
.catch(next);
}
There, what was so hard about that? Well, first of all the code is wrong. If validate() throws an Error, it goes up the stack instead of a 400. Second, on validation error doRealWork() is still called! The catch handles the error, and the Promise chain continues.
We can try to fix this by starting a Promise context and moving the catch down.
function(request, response, next) {
return Promise.resolve()
.then(() => validate())
.then(() => doRealWork())
.then((result) => response.send(result))
.catch((error) => response.status(400).send(error))
.catch(next);
}
Well obviously something is wrong here with a double catch. If doRealWork throws an error, it gets sent back as status 400 instead of going to next.
The real fix requires us to add a lot more code.
function(request, response, next) {
return Promise.resolve()
.then(() => validate())
.then(() => true)
.catch((error) => {
response.status(400).send(error);
return false;
})
.then((validationSuccess) => {
if (!validationSuccess) {
return;
}
return doRealWork()
.then((result) => response.send(result));
})
.catch(next);
}
We introduce a valid/invalid flag. And we also have to be careful to attach a then to doRealWork directly. If we move response.send(result) up one level, it would get called without a result if there was a validation error.
There is another way to write this code correctly using the second parameter of then, but folks, its not a looker.
// DO NOT NEST PROMISES LIKE THIS
function(request, response, next) {
return Promise.resolve()
.then(() => validate())
.then(() => doRealWork()
.then((result) => response.send(result), (error) => response.status(400).send(error))
.catch(next);
}
While that code is correct and short, it is extremely difficult to follow. This is a trap for the next person. They will try to simplify this code by unnesting the then and adding a catch.
function(request, response, next) {
return Promise.resolve()
.then(() => validate())
.then(() => doRealWork())
.then((result) => response.send(result))
.catch((error) => response.status(400).send(error))
.catch(next);
}
And guess what, its one of the wrong forms!
I hope this is an convincing argument on why to use async/await today!