July 19, 2020
JavaScript is a single-threaded programming language. A thread, short for thread of execution, is a context in which the language can do work. In a multi-threaded language, one thread can handle a task while another thread simultaneously can work an a separate task. Humans, for example, are multi-threaded: You can scratch your head with one hand while surfing the internet with the other. In JavaScript, this means that only one task be be on the thread at a time, before it is completed and the next one begins. To put it more simply, you can only do one thing at a time.
This makes writing the code somewhat less complex, as it allows you to avoid concurrency issues which can happen when the work being done on multiple threads interfere with each other. However, this also makes it difficult to perform long operations, such as requesting data from an API, without blocking the thread for an extended period of time.
Luckily, modern JavaScript has begun to implement features to handle these situations, known as asynchronous programming. These features include asynchronous callbacks, promises, and async/await functions. In this post, I am going to walk through the basics of promises and how to use them.
A promise is an special JavaScript object which will return a single value in the future: either a “resolved” value, or a reason it wasn’t resolved, such as an error. It is called a promise because it is essentially an object which “promises” you a value in the future, but doesn’t have it yet. Promises can exist in 3 states:
The basic syntax for constructing a promise looks ike this:
const promise = new Promise(function(resolve, reject) {// executor function});
The new Promise
syntax is know as the constructor, and it is used to create a new instance of a promise. The function we pass this constructor is known as the executor function, and will contain the work we want our promise to do. The executor function will be called automatically when the promise is created. The executor also takes two arguments, resolve
and reject
. These are callback functions pre-defined in JavaScript, and they are called in the executor depending on whether it was “successful” or not. If the executor was successful, it will call resolve
, and the promise state will be changed to fulfilled. If it produced an error, then it will call reject
and the state will be shifted to rejected. Here is an example of a promise function which will wait a specified amount of time before resolving:
const hello = time => new Promise((resolve) => setTimeout(resolve, time))
In this example, we use es6 arrow function syntax, and we omit the reject
argument. Both arguments are technically optional, so you can choose to only include the ones which you use.
All promises include a .then()
method which you can call on the promise in order to utilize the resolved or rejected value. We can use this on the promise defined above to print ‘Hello!’ to the console after waiting 3 seconds (3000 milliseconds), like so:
hello(3000).then(() => console.log("Hello!"))
As shown above, .then
accepts a callback function which is called after the promise is either resolved or rejected. This function is actually called with whatever value we pass into the resolve()
or reject()
function.
const resolveOrReject = (time, option) => new Promise((resolve, reject) => {if (option === "resolve") {setTimeout(() => resolve("resolved!"), time)} else if (option === "reject") {setTimeout(() => reject("rejected!"), time)}})resolveOrReject(3000, "resolve").then(((output) => console.log(output)))
This function will wait 3 seconds, and the log “resolved!” in the console if it was called with the resolve option. If it was called with the reject option, it will wait three seconds and then return an error with the message “rejected!”
If you use JavaScript to interact with other APIs, you are likely familiar with fetch()
. The Fetch API provides a JavaScript interface for handling HTTP requests and responses. The fetch()
function returns a promise, and is often one of the first places a JS developer will encounter promises and async programming. A typical fetch request will look something like this:
fetch('http://example.com/movies').then(response => response.json()).then(moviesObj => console.log(moviesObj));
As you can see, this process involves chaining .then()
callbacks to handle the promise returned by fetch()
and convert it into usable data. Using a promise ensures that JavaScript’s single thread isn’t completely blocked if the fetch request takes a while to return the data.
You know understand what a promise is, and how to use them. I hope you enjoyed this post, and that it provided some insight into the world of asynchronous programming in JavaScript!
The personal blog of Shane Lonergan. NYC based software engineer, actor, director, and musician. Documenting my journey from the stage to the computer screen. To keep up to date, you should follow me on Twitter.