How we got here
Promises marked a huge turning point in async js, they enabled a new type of control flow that saved us from callback hell. But some people found that calling .then()
multiple times was too much, too callbacky.
Then after a while, we resorted to generator functions and cogenerators, which made async code feel like its synchronous, at the cost of wrapping it in a generator function, yield
ing every line and introducing a cogenerator library (for example co) to deal with unwrapping the promises like the following example, where we could just yield
a promise whenever we encounter it and pretend that the yield
does not exist on that line of code.
co(function* () {
let result1 = yield somePromise1
let result1 = yield anotherPromise
dostuff(result1, result2)
})
This evolution served as the inspiration of the async/await
syntax introduced in es7, and finally we could just
let value = await somePromise
doStuff(value)
// instead of
somePromise.then(value => doStuff(value)
Oh, and you had to wrap it in an async
function to be able to use it, but that's changing with top level await
.
Why I use both
One simple reason: error handling.
Writing code for the happy path feels good, if only the world were a perfect place. But hélas, if you omit error handling during development, you will pay for it later while digging through a mysterious bug report.
Promises have a .catch(callback)
method similar to .then(callback)
where the callback
expects an error.
myPromise
.then(value => handleHappyPath(value))
.then(value2 => handleAnotherHappyPath(value2))
.catch(err => handleError(err))
The async/await
version looks like this:
try {
let value = await myPromise
let value2 = await handleHappyPath(value)
handleAnotherHappyPath(value2)
} catch(err) {
handleError(err)
}
One least used - but very useful - feature of .then
is that it accepts a second parameter as an error handler.
myPromise
.then(handleHappyPath, handleErrorScoped)
.then(anotherHappyPath)
.catch(err => handleError(err))
In this example, handleErrorScoped
will take care of errors for this particular step. While handleError
will handle errors of the whole chain (including errors inside handleErrorScoped
).
The equivalent sync/await
version requires a nested try/catch
block.
try {
let value
try {
value = await myPromise
} catch (err) {
// possibly setting `value` to something
handleErrorScoped(err)
}
let value2 = await handleHappyPath(value)
handleAnotherHappyPath(value2)
} catch(err) {
handleError(err)
}
Maybe it's just me, but I find the latter a hell of lot more verbose, running away from callback hell, ran directly into try/catch
hell.
An example of an instance where I found myself combining both is when I use puppeteer to check if an element exists in a page.
let hasElement = await page.evaluate(() => document.querySelector("some selector"))
.then(() => true)
.catch(() => false)
Conclusion
async/await
was a huge stepping stone towards simplifying async javascript, but it does not obsolete .then()
and .catch()
, both have their use cases, especially when
we need granular control over error handling.
A combination of both seems to give the most readable code, robust and maintainable code.
If you made it this far, please show your support with reactions and don't hesitate to
ask question within comments, I'd love to answer each one of them and know your thoughts about the dichotomy of async/await
vs .then()
🙂
Top comments (9)
One other thing to mention about this example:
Is that it can be written like this as well 💖
The good thing about being callbacks, is that we don't have to do
.then(arg => fun(arg))
, we can just do.then(fun)
. The approach withasync
/await
is far from being that neat. Like you I tend to use.then
way more often, and just use async when it actually helps with readability.Yes, I do that too, I used this style for clarity.
async/await
forces you to have a lot of intermediary variable names.So fresh and so clean-clean...
Several people told me that async/await was the right thing to do today, but I'm often more comfortable with then() ... As you said it will depend of your situation but then() is definitely still useful
People who come to javascript from languages with limited support for higher order functions often hate
.then()
A single catch block is a much better approach anyway IMHO. Too many catch blocks is a waste if energy and not very good design from a DRY perspective.
I agree, but sometimes you need that granular control over error handling
Those are very rare cases, and usually bad design. You want to centralize your error handling so changing it isn't a seek and destroy mission. :)
for me it's more about the second arg to
.then()
in that case, but I get your point.