Asynchronous JavaScript, which is JavaScript that uses callbacks, promises, and async/await, helps with functions that take time to return some value or to produce some result. This article gives a basic explanation of how callbacks, promises, and async/await work, and it also helps anyone who has struggled with unreadable callbacks.

Callbacks

A callback function is usually used as a parameter to another function. The function that receives the callback function as a parameter is normally fetching data from a database, downloading a file, making an API request, or completing some other task that could block the code thread for a notable amount of time.

Imagine, for example, that you are going to download an image. The download takes about 2 seconds, and you don't want your program to stop on this line of execution as you wait for the download to finish. A callback allows you to keep running other functions and "call back" the image after the download is complete.

Let's look at an example using the setTimeout function to simulate the image download. (The colors on the right are the logs of the codes.)

As we can see, the second() function only runs after 2 seconds because of the callback function on the setTimeout function. While the download is happening, the executions of the first() function continue.

  • Therefore, callback functions:
  • Allow functions to run in the "background;"
  • Are called when the function finishes its work;
  • Are non-blocking.

However, with great power comes great irresponsibility ;). Some functions that use asynchronous calls can end badly, like this one:

 

Image courtesy of callbackhell

As we can see, this code is hard to understand and maintain, and it isn't scalable. This is what we call callback hell. (There's even a site called callbackhell that explains this subject in detail.) 

In modern JavaScript, we have two approaches that "promise" to end this suffering. These are called "promises" and "async/await."

Promises

A promise is an object that represents something that will be available in the future. In programming, this "something" is values. Promises propose that instead of waiting for the value we want (e.g. the image download), we receive something that represents the value in that instant so that we can "get on with our lives" and then at some point go back and use the value generated by this promise.

Promises are based on time events and have some states that classify these events:

  • Pending: before the event happens;
  • Settled/Resolved: after the event happens;
  • Fulfilled: when the promise returns the correct result;
  • Rejected: when the promise does not return the correct result.

We can produce or consume promises. When we produce a promise, we create a new promise and send a result using that promise. When we consume a promise, we use callback functions for the fulfilled and rejected states of that promise.

In the example above, we are producing a promise that looks for the IDs of employees. You may notice that the promise object receives a callback function that accepts two arguments: resolve and reject. This callback is called an executor function, and it is called immediately when the promise is created.

The executor function informs the promise of whether or not the event was successful. If it was successful, the function "resolve" is called. If it was unsuccessful, the "reject" function is called.

Now let's consume our promise. For this, there are two methods that we will use: then () and catch (). All promises inherit these two methods.

The then () method from the returned object allows us to add an event handler when the promise reaches the state of fulfilled, which means its success state, thus returning the result of the function, "resolve," called by the executor function.

So what we do is pass a callback function that will handle the returned data. This callback function receives an argument--in our example "IDs." The "IDs" argument is a result of the promise, and the return of our function that receives "IDs" resolves our Array. 

The catch method works the same way, but the expected return is from the "reject" function.

Well then, the basic principle of the promises to end callback hell are:

  • A promise will gain control over the results of callbacks: resolve and reject functions;
  • All promise objects have the then () method.

Promises have a great advantage (if used correctly) called chaining. With chaining, we can simply add a new then () method after a then (). This gives us greater control over our chain of resolved promises.

But we have to be careful that the spell does not come back against the sorcerer, for there is a promise callback hell. Imagine a case where we are authenticating a user (merely illustrative):

Can you identify in the consumption of this promise the pattern of a callback hell? We have one call within another, and if more entries are added, this code may become unreadable and difficult to maintain.

So it's always good to research and keep in mind good implementation practices for promises. For example, in some cases, we can solve all the promises at once:

But just use Promise.all () when the order of the promises is not relevant. We can also serialize the promises in sequence:

This shorthand syntax works because the then () method returns a promise. Here's a helpful article detailing good practices to follow.

Async/Await

Async /await is another alternative for consuming promises, and it was implemented in ES8, or ES2017.

Async/await is a new way of writing promises that are based on asynchronous code but make asynchronous code look and behave more like synchronous code. This is where the magic happens.

So here's what happened in the code above: we created an async function using the async keyword before the function. This means that this function is asynchronous, which means it basically runs in the background.

So what's happening in this async function?

An async function may have one or more await expressions, so what we did was to consume our promises with the expression "await," which returns the result of our resolved function called through the promise.

This is basically a top-down way of handling asynchronous cases since it runs in the background.

One important thing to note is that an async function returns a promise. So to return the values of the getEmployee () function, we need to use the then () method, thus returning the expected value--that's why it is important to know the concept and understand how promises work as well.

That's all you need to know as an intro to callbacks, promises, and async/await! Let me know how it goes for you!


Author

Bruno Dias

I am a consultant and front-end developer with 5+ years' experience.


[JAVA 21] Structured Concurrency: Powering Data Orchestration with Virtual Threads and Scopes

READ MORE

How to Build Unit Tests Using Jest

READ MORE

How to Create Your Own RAR Extractor Using Electron

READ MORE

How to Create a Tic-Tac-Toe Board Using React.js

READ MORE