JavaScript Concurrency: Promise Constructor
Welcome to the Promise Constructor lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
So far, we've created our initial promises with
Promise.resolveandPromise.reject. These methods work in many straightforward situations, so it's a good idea to use them.However, there are times when we need more flexibility. For example, we may want a promise to wait for a network, a disk, or a timer. Using what we've seen so far, we might try to call
Promise.resolveinside of asetTimeout.>
function runInOneSecond() {setTimeout(() => {Promise.resolve().then(() => console.log('then is running'));}, 1000);}console.log('before');runInOneSecond();console.log('after');async console outputThis approach successfully delayed the promise execution, but we still have a problem. When
runInOneSecondstarts to run, it only sets a timer, but doesn't create the promise. Then, at the 1000 ms mark, the timer finishes. The promise is created and immediately resolved. The promise resolves inside thesetTimeout, so ourrunInOneSecondfunction can't return it.If
runInOneSeconddoesn't return a promise, thenrunInOneSecond().then(...)won't work. We've lost composability, one of the biggest benefits of promises.(Composability means that we can arrange system components in different ways. In our case, it means that we can chain
.then(...)to do more work afterrunInOneSecondfinishes.)Fortunately, it's possible to compose promises with timeouts by using the
new Promiseconstructor. It lets us create a promise now, but resolve it later.>
function ourCallback(resolve) {resolve(5);}new Promise(ourCallback);Async Result:
We pass the
ourCallbackfunction to thenew Promiseconstructor, which calls it immediately. Our callback gets aresolvefunction as an argument. When we callresolve, the promise fulfills.Above, we defined our callback separately and passed it in. We did that for clarity, but in most cases the callback is written inline.
>
new Promise(resolve => resolve(5));Async Result:
{fulfilled: 5}We can call
resolve()immediately, as in the examples above. However, the real power is in calling it later. This is the opposite approach from what we tried before. Rather than creating a promise inside thesetTimeout, we're going to usesetTimeoutinside the promise.In the next example, we create a promise and attach some
thens to it. We then usesetTimeoutto wait for one second before fulfilling the original promise. Once that second is up, the promise fulfills, so both "downstream"thens also fulfill. This is an important point: when a promise waits for something, all of itsthens also have to wait!>
console.log('before');new Promise(resolve => {setTimeout(resolve, 1000);}).then(() => {console.log('first then is running');}).then(() => {console.log('second then is running');});console.log('after');async console outputThat solves our problem! We now have a regular-looking promise that fulfills after a delay, and runs its
thens once it fulfills.In the example above, we created an artificial delay with
setTimeout. However, analogous situations are common in real-world file and network operations, where we write code like:>
db.getUser(5).then(user => {db.updateUser(user, {emailConfirmed: true});return user;}).then(user => {// Email the user saying that they're confirmed.sendEmailConfirmedMail(user);});In code like that, the
getUser,updateUser, andsendEmailConfirmedMailfunctions all ultimately rely on thenew Promiseconstructor that we just saw. They're waiting for network operations instead of timers, but the principle is the same.Here's a code problem:
Build a new promise with
new Promise, putting the value'it worked'inside it. We've included some code that adds athento convert the string into SHOUTING! (It's important to celebrate code that works!)new Promise(resolve => resolve('it worked')).then(string => string.toUpperCase() + '!');Async Result:
- Goal:
{fulfilled: 'IT WORKED!'}- Yours:
{fulfilled: 'IT WORKED!'}
We can also use the
new Promiseconstructor to create a rejected promise. To do that, we accept a second argument in our callback,reject.>
new Promise((resolve, reject) => reject(new Error('it failed')));Async Result:
>
new Promise((resolve, reject) => reject(new Error('no such user')));Async Result:
{rejected: 'Error: no such user'}This allows us to accept or reject a promise depending on what happens at runtime.
(The next example uses JavaScript's
.findarray method. It returns the first array element that matches the given function.)>
const users = [{id: 1, name: 'Amir'},{id: 2, name: 'Betty'},];function getUser(id) {return new Promise((resolve, reject) => {const user = users.find(user => user.id === id);if (user) {resolve(user);} else {reject(new Error('no such user'));}});}- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
getUser(1);Async Result:
{fulfilled: {id: 1, name: 'Amir'}} - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
getUser(3);Async Result:
{rejected: 'Error: no such user'} One important thing to keep in mind: the
resolvefunction that we get from the constructor is separate from thePromise.resolvemethod. And therejectfunction here is separate from thePromise.rejectmethod.You can think of
Promise.resolveandPromise.rejectas the simpler versions: when we callPromise.resolve(5), we're creating a new promise that's immediately fulfilled with the value5. We don't need to pass in a callback function, but the trade-off is that there's also no way for us to delay resolution.The promise constructor is the more complex version: when we do
new Promise(resolve => ...), we can delay our call toresolve. We can even save theresolvefunction and call it from another part of the system, which we'll see in a future lesson.Finally, a quick note about terminology. The
(resolve, reject) => ...function that we pass tonew Promiseis called the "executor". We rarely need to talk about it directly, but if you see "executor" mentioned in discussions of promises, this is why.