JavaScript is an asynchronous programming language. This means that it can only execute one statement at any given time.
Unfortunately, this makes it easy to introduce unintended bugs into your JavaScript code.
In this tutorial, you will learn how asynchronous JavaScript works and how to avoid its problems using a JavaScript feature called promises.
Table of Contents
You can skip to a specific section of this JavaScript tutorial using the table of contents below:
- Everything in JavaScript is Asynchronous
- JavaScript Promises
- Adding Delays to JavaScript Promises
- How to Access the
resolve
andreject
Values in a JavaScript Promise Using thethen
Keyword - Chaining Together Promises
- Final Thoughts
Everything in JavaScript is Asynchronous
Before we learn how to create promises in JavaScript, you should first understand what it means to say that everything in JavaScript is asynchronous.
To start, let's discuss how JavaScript is a single-threaded language. This means that only one statement can be run at a given time.
On the other hand, certain other programming languages are multi-threaded. This means they are capable of executing multiple statements concurrently.
The single-threaded nature of JavaScript can introduce some unintended bugs if you do not have a thorough understanding of how it works. This is especially true if you introduce any timeouts into your code. Let's visualize this with some JavaScript code!
First, let's log two strings to the console:
console.log('The start of our program');
console.log('The end of our program');
By this point in the course, you should understand that the following output will now appear in the console:
The start of our program
The end of our program
Between these statements, let's add an instance of the setTimeout
function, which accepts two arguments: a callback function, and an amount of time to wait before executing the callback function. We'll keep things simple and add a callback function that logs The middle of our program
to the console after waiting for 5 seconds (or 5000 milliseconds), like this:
console.log('The start of our program');
setTimeout(() => console.log('The middle of our program'), 5000);
console.log('The end of our program');
What do you think happens when you run this code? After waiting for five seconds, here is what appears in our console:
The start of our program
The end of our program
The middle of our program
What gives?
Well, the asynchronous nature of JavaScript means that the script does not stop running after it hits the setTimeout
function. It moves on to the next console.log
statement and places its own callback function into something called the Callback Queue
.
Functions that are waiting in the Callback Queue are run whenever your JavaScript application is not busy running any other code.
To fix this problem, you could refactor your code so that the console.log
statements for both The middle of our program
and The end of our program
are within the setTimeout
function.
Here is what I mean:
console.log('The start of our program');
setTimeout(function () {
console.log('The middle of our program');
console.log('The end of our program');
}
, 5000);
Overall, the main takeaway that you should understand from this section is that JavaScript is single-threaded, which means that it can only run one function at a time. This can introduce unintended functionality when paired with setTimeout
functions and other built-in timer functionality.
The solution to this is JavaScript promises, which we explore in the next section of this tutorial.
JavaScript Promises
JavaScript promises allow you to execute code after waiting for something else to happen. Promises are created immediately, but do not resolve until they are ready. Whether or not a promise is ready to resolve is dependent on some outside factor - whether data has been imported, for example.
To illustrate how promises work in JavaScript, let's create a function called makePie
. This function will contain a promise called piePromise
and will return this promise using the return
keyword.
function makePie(){
const piePromise = new Promise();
return piePromise;
}
This is not just a silly example - it parallels the real-world situation of ordering a pie from a bakery. When you call in to a bakery and order a pie, the pie is not given to you immediately. Instead, you rely on the promise of the bakery that the pie will be delivered when it's ready - once the ingredients have been put together and the pie has actually been baked.
Let's return to the makePie
function. The promise that it contains isn't actually done yet. JavaScirpt promises accept functions as their arguments. Those functions require two parameters:
- The
resolve
function - the function that will be executed when the promise is resolved. - The
reject
function - the function that will be executed if an error occurs with the promise.
Let's add this functionality to the piePromise
promise. To keep it simple, we will pass in Your pie is ready!
to the resolve
function and Something went wrong with your pie!
to the reject function:
function makePie(){
const piePromise = new Promise(function(resolve, reject){
resolve('Your pie is ready!');
reject('Something went wrong with your pie!')
});
return piePromise;
}
Let's modify this makePie
function further so that it accepts a pieType
argument - think Apple, Pumpkin, or your personal favorite type of pie. After that, we can interpolate in this pieType
parameter to the resolve
and reject
functions.
function makePie(pieType){
const piePromise = new Promise(function(resolve, reject){
resolve(`Your ${pieType} pie is ready!`);
reject(`Something went wrong with your ${pieType} pie!`)
});
return piePromise;
}
Now what happens when we run this function?
Well, if you run makePie('Apple')
, here is the output:
Promise {<resolved>: "Your Apple pie is ready!"}
This shows that the promise was resolved.
Adding Delays to JavaScript Promises
So far, we have built a function called makePie
that contains a piePromise
promise. However, our promise is missing an important piece of functionality - the ability to wait before executing.
To add this functionality, we need to add a setTimeout
function within piePromise
. Here's how you would do this with a 2 second (or 2000 millisecond) delay:
function makePie(pieType){
const piePromise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(`Your ${pieType} pie is ready!`);
reject(`Something went wrong with your ${pieType} pie!`)},
2000
);
});
return piePromise;
}
Now if you run makePie('apple')
and immediately expand the object that it returns, you will see the following:
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
However, if you wait two seconds (or more) before doing this, you will instead see:
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: "Your apple pie is ready!"
This shows that we have successfully added a delay to our JavaScript promise! In the next section, we will see how
How to Access the resolve
and reject
Values in a JavaScript Promise Using the then
Keyword
In the last section of this tutorial, we were working with the following function:
function makePie(pieType){
const piePromise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(`Your ${pieType} pie is ready!`);
reject(`Something went wrong with your ${pieType} pie!`)},
2000
);
});
return piePromise;
}
Although we have added the resolve
and reject
values to this promise, we have not yet seen how to access these values.
Fortunately, it is very easy. Once the promise has been resolved, you can access the values by using the then
method. The then
method accepts a function and passes in the promise as the argument to that promise. If you log the promise to the console within the then
method, you will access either the resolve
or reject
statement.
Here is an example:
makePie('apple').then(pie => console.log(pie))
//Logs 'Your apple pie is ready!' to the console once the promise is resolved
We will learn more about the then
method later in this tutorial.
Chaining Together Promises
Earlier in this tutorial, we learned how the setTimeout
function combined with JavaScript's asynchronous nature can lead to bugs in your JavaScript code.
Using the setTimeout
to create delays in JavaScript becomes especially problematic when you want to chain together multiple delays. To do this, you need to nest multiple setTimeout
functions inside of each other, like this:
(function(){
setTimeout(function(){
console.log("Hey.");
setTimeout(function(){
console.log("Hey again.");
},2000);
},3000);
})();
This quickly becomes unreadable if you scale this up further.
Fortunately, promises provide a better way for chaining together multiple delays in JavaScript. After creating a promise in JavaScript, you can use the then
method to execute another function after the promise is resolved.
As an example, consider the following code:
This code creates the makePie
function, then creates a promise and saves it to the variable thePiePromise
. The code then invokes the then
method on thePiePromise
and logs End
to the console.
function makePie(pieType){
const piePromise = new Promise(function(resolve, reject){
setTimeout(function () {
resolve(`Your ${pieType} pie is ready!`);
reject(`Something went wrong with your ${pieType} pie!`);
},2000
);
});
return piePromise;
}
const thePiePromise = makePie('apple')
console.log('Start')
thePiePromise.then(() => console.log('Pie is done'))
console.log('End')
Here's what will be generated in the console when you run this code:
Start
End
Pie is done
Of course, the last line is only generated after 2 seconds (when the promise is resolved).
Now let's refactor this code so that End
is logged to the console 2 seconds after Pie is done
is.
The first thing we need to do is modify the function within our then
method so that it is no longer an arrow function.
const thePiePromise = makePie('apple')
console.log('Start')
thePiePromise.then(function (){
console.log('Pie is done')
})
console.log('End')
Once this is done, we need to return another invocation of the makePie
function.
const thePiePromise = makePie('apple')
console.log('Start')
thePiePromise.then(function (){
console.log('Pie is done')
return makePie()
})
console.log('End')
This will trigger the setTimeout
function for another 2000 milliseconds.
Now we can chain another invocation of the then
method, and within that then
method, we can add an easy arrow function that logs End
to the console.
Here's the code:
const thePiePromise = makePie('apple')
console.log('Start')
thePiePromise.then(function (){
console.log('Pie is done')
return makePie()
}).then(() => console.log('End'))
Boom! We're done! Once you run this code, you will see the following output;
Start
Pie is done
End
Because of the setTimeout
function contained within the makePie
function, the second line appears after 2 seconds and the last line appears after 4 seconds.
Final Thoughts
In this tutorial, you learned about the asynchronous nature of JavaScript code. You also learned how to write and manipulate JavaScript promises.
Here is a brief summary of what we discussed in this lesson:
- What it means to say that "everything in JavaScript is asynchronous"
- How JavaScript's asynchronous nature can introduce bugs, especially when using the
setTimeout
function - How to write JavaScript promises
- How to use promises to trigger delayed functionality using the
then
method - How to chain together promises using multiple
then
methods