JavaScript Concurrency: Catching Promise Rejections
Welcome to the Catching Promise Rejections lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In regular JavaScript, we recover from exceptions with
catch.>
function tryAndCatch() {try {throw new Error();} catch (e) {return 'caught';}}tryAndCatch();Result:
'caught'
We can catch promise rejection as well, but not with the usual
try { ... } catch { ... }syntax. Instead, promises have a.catch(...)method. When a promise rejects, itscatchcallback (if any) is called.In some ways,
catchbehaves likethen. Both take a callback function, and both return promises containing the callback's return value. The main difference is thatthenis called under normal conditions, whilecatchis called only when a promise rejects. Withcatch, we can recover from a rejection, and return a fulfilled promise rather than a rejected one.>
Promise.resolve().then(() => {throw new Error('this will reject!');}).catch(() => 'caught');Async Result:
>
const users = [];function firstUserName() {if (users.length === 0) {throw new Error('there are no users');} else {return users[0].name;}}// Return a promise containing the first user's name, or "undefined" if there// are no users.Promise.resolve().then(() => firstUserName()).catch(() => undefined);Async Result:
{fulfilled: undefined}This is an important point that's easy to forget:
catchhandles rejected promises, but it usually returns a fulfilled promise, as it did above by returning{fulfilled: undefined}!Here's a code problem:
A user is making a purchase and enters an invalid discount code. The code here looks for the discount code in the "database" (we use an array instead of a real database). If the discount code doesn't exist, we return a rejected promise.
Unfortunately, the rejected promise will cause our application to return an HTTP 500 error. We don't want that, so we need to handle the rejection. Handle the rejected promise with
catch, returning a promise that fulfills withundefinedinstead.const discounts = [{code: 'first-month-free'},{code: 'annual-sale'},];function findDiscountCode(code) {const discount = discounts.find(d => d.code === code);if (discount === undefined) {return Promise.reject(new Error('discount does not exist'));} else {return Promise.resolve(discount);}}findDiscountCode('please-give-everything-to-me-for-free').catch(error => undefined);Async Result:
- Goal:
{fulfilled: undefined}- Yours:
{fulfilled: undefined}
The
catchmethod is the most common error-handling mechanism in promises. But there's another, less common method as well.So far, our
thencallbacks have always taken one callback function.>
Promise.resolve(['one', 'two']).then(value => value.length);Async Result:
{fulfilled: 2}But
thenactually takes two callback functions as arguments:.then(onFulfilled, onRejected). TheonRejectedcallback is called when the parent promise rejects.>
Promise.reject(new Error('user does not exist')).then(value => 'it fulfilled',reason => 'it rejected');Async Result:
A promise can fulfill or reject, but it can't do both. Only one of the two
thencallbacks is called: eitheronFulfilledoronRejected.Finally, a note about the relationship between rejection and errors. In most cases, rejection is used to indicate an error: something genuinely went wrong. But we can use it for other purposes too.
For example, imagine that we're building a web-based email product. We want to let users import their email archives from other email providers. We do that by connecting to the other provider's mail servers and downloading all of the user's email. There may be hundreds of thousands of emails, so this can take hours. We want to allow the user to abort the import if needed.
We can implement our email import as two chained promises:
- Download all of the email from the user's email server, storing it in a temporary location. (This is the slow part!)
then, after all of the email is downloaded, save it in our own email database. (This is much faster than the download.)
If the user asks us to abort the import during phase 1, we can reject that promise. That prevents phase 2 (save the emails in our database), so it's as if we never started. We rejected a promise, but that rejection was due to a user's request, not due to an error.