An asynchronous journey
Promises are a core feature in JavaScript. They’ve been around all the way back in 2015 when ES6 became a standard. Prior to that, libraries had their own implementations of asynchronous code. All code used in this post is available at my GitHub.
So asynchronous is a term that’s been around for quite some time. There are other people that are better at explaining how it works and I’d just be repeating that. However, I’d suggest the following resources:
How I like to view it in short is:
The example of such code can be:
// synchronous
function getMyValueSync() {
return 20;
}
function myFunction() {
const a = getMyValueSync();
console.log(a);
}
console.log('First log');
myFunction(); // Second log - 20
console.log('Third log')
Now, to make this simple synchronous code async, let’s use Promises
:
// asynchronous
async function getMyValueAsync() {
return 20;
}
async function myFunction() {
const a = await getMyValueAsync();
console.log(a);
}
(async () => {
console.log("First log");
myFunction(); // Actually third log - 20
console.log("Second log");
})();
In the previous example, I’ve used Immediately Invoked Function Expression, or IIFE for short. This allows me to do the example in Node.JS directly. You can read about them here.
Just like that, using the exact same code, yet putting there a bunch of async
and await
keywords, we’ve
made the code asynchronous. With synchronous code, we’ve logged:
With asynchronous code, we’ve logged:
Note: You can (and, in fact, should) await the
myFunction
call in the last code block.
I’ve purposely omitted it to show the dangers of async code - that 2 lines in succession might not be executed as expected
A promise has 3 states:
pending
- The Promise
has not been neither resolved
not rejected
fulfilled
- The Promise
has reached a final state through resolve
rejected
- The Promise
has reached a final state through reject
There are 2 ways to created a Promise
While I prefer the latter, it is beneficial to understand the first one to see what exactly is happening inside and why we need multiple ways of creating them.
Let’s create a couple Promises
and look at the states defined above:
(() => {
const myPendingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 500);
});
console.log(myPendingPromise)
})
When creating a Promise
and don’t wait for it to finish, when we log it immediately, we can see in console Promise { <pending> }
.
Now, let’s extend the code and see the state it is in after awaiting it:
(async () => {
const myPendingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 500);
});
const myFulfilledPromise = new Promise((resolve, reject) => resolve());
await myFulfilledPromise;
console.log(myPendingPromise);
console.log(myFulfilledPromise);
})();
In the console, now we’ll see:
Promise { <pending> }
- The Promise
has been defined, but the resolve
is yet to be called because of timeout.Promise { undefined }
- The Promise
has been defined and resolved immediately. This Promise
is fulfilled
and holds a value.Note the await myFulfilledPromise
. If I were to await
on previous line, I’d have gotten the value undefined
and could no longer log the state of the Promise
.
Finally, let’s explore the rejected
state. For the fulfilled, we’ve used the resolve
callback.
For the rejected
, we’ll need to use the reject
function. Consider the following code
(async () => {
const myRejectedPromise = new Promise((resolve, reject) => reject());
})()
Now, unfortunately, this part is not going to work. That is because rejecting a promise throws an error. So, let’s wrap it in a try/catch
block:
(async () => {
try {
const myRejectedPromise = new Promise((resolve, reject) => reject());
} catch (e) {
console.log(e);
}
})()
If we try to run this code, we’ll again reach the same error - UnhandledPromiseRejection
. But that is because Promises
are asynchronous! Let’s await
it then:
(async () => {
try {
const myRejectedPromise = new Promise((resolve, reject) => reject());
await myRejectedPromise;
} catch (e) {
console.log(e);
}
})()
In this case, the Promise
was rejected
. After exploring all these states, let’s put the code all together:
(async () => {
const myPendingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 500);
});
const myFulfilledPromise = new Promise((resolve, reject) => resolve());
await myFulfilledPromise;
console.log(myFulfilledPromise)
try {
const myRejectedPromise = new Promise((resolve, reject) => reject());
await myRejectedPromise;
} catch (e) {
console.log(e);
}
})();
So, in the examples before, I’ve used the Promise
constructor rather than async await
to create a Promise
.
I’ve also said that you can create a Promise
through those.
So, the important thing about async/await
is that it applies to functions. Consider the following example:
const promise = new Promise((resolve, reject) => resolve(20));
console.log(promise) // Promise { 20 }
In the example above, I’ve created a Promise
that holds a value 20 using Promise constructor
.
To create a Promise
that holds a value 20 with async/await
, I can simply do this:
async function createPromise(val) {
return val;
}
const promise = createPromise(20);
console.log(promise); // Promise { 20 }
To reject a promise, I’d do:
const promise = new Promise((resolve, reject) => reject());
try {
await promise;
} catch (e) {
console.log(e); // undefined
}
However, with async/await
, I’m going to throw
:
async function createPromise() {
throw undefined;
}
try {
const promise = await createPromise();
} catch (e) {
console.log(e); // undefined
}
I believe that is enough for now. If you’d like to know how the world looked without async/await
and it can kill your application,
read more in the part 2 of Let’s talk promises