Welcome to another entry in my Practical Promises series! I know I said the next post would be the last, and we would talk about async/await, but I changed my mind! Instead, today, we’ll revisit the fundamentals to show just how important it is to call things in the right order.
[more]
If you are just joining us, here is what you have missed so far:
In part 1, we talked about what promises are and what they can be used for.
In part 2, we started looking at how we can create promises.
Then in part 3, we saw how each call to then actually makes a new promise, and that those promises can be chained together.
In part 4, we learned how to combine promise chaining with the creation of new promises in order to simplify complex async workflows.
In part 5, we applied everything we have learned so far to create a nice, clean API that unwraps a complex result object via promise chaining.
These Things are Not Equal
Waaaaaaay back in part 1, we learned that about the then
and catch
functions. We also briefly mentioned that you can actually register an error callback using an “overloaded” version of then
, like so:
const promise = Promise.reject('DENIED');
promise.then(x => console.log(`Success! ${x}`), x => console.log(`Failed! ${x}`));
You might assume the above code is equivalent to the following:
const promise = Promise.reject('DENIED');
promise.then(x => console.log(`Success! ${x}`))
.catch(x => console.log(`Failed! ${x}`));
And for this simple example, you would be correct. Both give the same output:
Failed! DENIED
But what about these two examples?
const promise = Promise.resolve('APPROVED');
promise.then(x => Promise.reject('DENIED!'), x => console.log(`Failed! ${x}`));
const promise = Promise.resolve('APPROVED');
promise.then(x => Promise.reject('DENIED!'))
.catch(x => console.log(`Failed! ${x}`));
Will they give the same output?
No, they will not.
The first one will actually raise an unhandled error:
The second one will hit our catch
callback and log the error to the console:
Failed! DENIED!
So what gives? We’re registering an error callback in both cases, right??
Not so fast.
Let’s break it down and look at what makes these two seemingly equivalent samples different from one another.
What then
Actually Does…
Think back to what we learned in part 3: every time we call then
, we’re actually creating a new promise. That new promise is linked to our original one, but it is still a new promise.
To better illustrate, we could rewrite our last example like so:
const promise = Promise.resolve('APPROVED');
const newPromise = promise.then(x => Promise.reject('DENIED!'));
newPromise.catch(x => console.log(`Failed! ${x}`));
See the difference? We’re actually registering a catch
callback on our new promise. That catch will be executed if our new promise fails or if the original fails (thanks to promise chaining).
There’s one more really important point I want to make, too: then(success).catch(error)
is not the same as catch(error).then(success)
, either!
What catch
Actually Does…
catch
actually behaves just like then
does! It returns a new promise! We can verify this for ourselves:
const originalPromise = Promise.resolve(1);
const catchPromise = originalPromise.catch(x => console.log(`Caught: ${x}`));
console.log('Are they equal? ' + (catchPromise === originalPromise ? 'Yes!' : 'No!'));
//Output: Are they equal? No!
This has an interesting implication when it comes to ordering. What will the following example give us?
const promise = Promise.resolve(1);
promise.catch(x => console.log(`Caught: ${x}`))
.then(x => console.log(`Resolved: ${x}`));
Thanks to promise chaining, we’ll actually get the result we want:
Resolved: 1
BUT, as you probably expect by now, our catch
won’t actually handle any errors that occur within our then
callback:
const promise = Promise.resolve(1);
promise.catch(x => console.log(`Caught: ${x}`))
.then(x => Promise.reject('DENIED!'));
This example will throw an unhandled exception.
Because of that, I like to structure my code with a catch
callback at the end, followed by (if necessary) a finally
callback:
someAsyncAction()
.then(result => { /* do something async!*/ })
.then(result => { /* do something else async!*/ })
.catch(error => { /* Handle **all** errors!*/ })
.finally(() => { /* Do any final cleanup or handling. */ })
“Hey, what’s that finally thing??” you ask? Great question!
I actually didn’t focus on that yet. I guess we should do that in the next post!While the standard ES6Promise
type does not expose afinally
function, many common Promise libraries for JavaScript do. We’ll learn more about howfinally
works in these libraries and how to mimic it when using standard ES6 promises.
So Remember…
The important takeaway here is this:
then(success, error)
is not the same as then(success).catch(error)
, and neither of those is the same as catch(error).then(success)
! Order does matter here. Just remember that each call to then
, catch
, and even finally
(supported libraries only) actually creates a new promise, and you’ll be fine!
Up Next…
I lied. Again. Instead of wrapping up with async and await, we actually need to cover the finally
function that’s available in a lot of common promise libraries. Don’t worry, it’s simple and builds on what we’ve already talked about, so it won’t take long! Then we can wrap up with async and await, I promise
! ?