Asynchronous programming in JavaScript: A beginner’s guide

Oladipupo Ishola
10 min readFeb 25, 2023
Image from Ayush Saini

As a beginner, some JavaScript concepts took me a while to grasp, and asynchronous programming in JS is one of them. There are times when developing locally and testing them in the browser causes my browser to lag or freeze while running, and I'm not sure why. Nonetheless, I later discovered that my code was performing heavy computational tasks that were preventing the main thread from running.

If you see something like the image below, it means that something is preventing the main thread from responding to the UI response, or your computer is really slow, and you should consider an upgrade. Joking!

Asynchronous programming is an essential part of modern web development. It enables JavaScript code to perform tasks like retrieving data from a server or animating a web page without interfering with the main thread. In this article, I'll cover everything you need to know to get started with asynchronous programming in JavaScript. But first, let us briefly discuss the main thread.

The so-called Main Thread

The main thread is a term used in computer science, specifically in web development, to refer to a program's primary execution context. The main thread in JavaScript refers to the thread that executes the code of the web page or web application in the user's browser.

All JavaScript code runs on the main thread, which means that any time-consuming tasks or blocking operations can cause the web page or application to become unresponsive or "hang". This is because the main thread is in charge of handling user input, updating the UI, and running JavaScript code, and any time spent on a time-consuming task can result in delays in these critical functions. Check out this article for a more in-depth explanation of the main thread.

This is where asynchronous programming comes in, as it allows developers to move time-consuming or blocking operations off of the main thread and onto background threads, freeing up the main thread for user input and UI updates. This can significantly improve web application performance and responsiveness, making them feel faster and more interactive to users.

Developers must be mindful of the main thread and avoid clogging it with time-consuming tasks. The use of asynchronous programming techniques such as Promises and async/await to perform time-consuming tasks in the background while leaving the main thread free to handle user input and update the UI is a common technique for avoiding blocking the main thread.

Overall, understanding the main thread and how to work with it using asynchronous programming techniques is an important skill for web developers that can greatly improve web application performance and user experience.

Before we get into the specifics of asynchronous programming in JavaScript, let's first define the terms "synchronous" and "asynchronous" programming.

Synchronous vs. asynchronous programming

Synchronous programming is the traditional approach to programming, in which code is executed in sequential order. When a piece of code is executed, the program waits for it to finish before proceeding to the next piece of code.

In contrast, asynchronous programming allows code to run out of order. When asynchronous code is executed, the program does not wait for that code to finish before moving on to the next piece of code.

I believe we now have a basic definition of the term "asynchronous." You're probably wondering how it works, right?

How Asynchronous Programming Works in JavaScript

Asynchronous programming in JavaScript is implemented using a variety of techniques, including callbacks, promises, and async/await, which we will go over with some basic examples.

  1. Event Loop

The event loop is at the heart of asynchronous programming in JavaScript. The event loop is a mechanism in JavaScript that allows it to handle multiple tasks at the same time.

When an asynchronous task is completed, it is added to the event loop. The event loop checks the queue regularly for tasks that are ready to be executed. When a task is complete, it is executed, and the event loop moves on to the next task.

The Event Loop is essentially in charge of constantly monitoring the program’s execution context, including the main thread and any background threads, and looking for any pending events or tasks that need to be processed.

Here’s a step-by-step breakdown of how the Event Loop works in JavaScript:

  1. JavaScript code is executed on the main thread, which runs the code of the user’s browser’s web page or web application.
  2. When an asynchronous task, such as a network request or timer, is initiated, it is transferred from the main thread to a background thread or the browser’s internal task queue.
  3. While the asynchronous task is running, the main thread continues to process user input and other tasks.
  4. When the asynchronous task is finished, it is added to the task queue of the browser.
  5. The Event Loop is constantly scanning the task queue for any uncompleted tasks.
  6. When a task is discovered in the task queue, the Event Loop transfers it to the main thread for processing.
  7. The task is completed by the main thread, which may include updating the UI or performing other operations before returning to the Event Loop.
  8. The Event Loop repeats this process, continually monitoring the task queue for any pending tasks and moving them back onto the main thread for processing.

The “Event Loop algorithm” refers to the process of constantly monitoring the task queue and moving tasks onto the main thread as they become available. It enables JavaScript code to be executed asynchronously, allowing web applications to handle events and perform time-consuming tasks without blocking the main thread and rendering the application unresponsive.

In a nutshell, the Event Loop algorithm goes thus;

Step 1: While there are tasks

  • execute them, starting with the oldest tasks

Step 2: Then sleeps until a task appears

  • then go to step 1

2. Callbacks

Callbacks are one of JavaScript's oldest and most widely used asynchronous programming techniques. A callback is a function that’s passed as an argument to another function. When the original function finishes, it calls the callback function to indicate that it is finished.

Here’s an example of using a callback to fetch data from a server:

fetch('https://example.com/data')
.then(response => response.json())
.then(data => {
// Do something with the data
});

In this example, fetch() is an asynchronous function that returns a Promise. When the Promise is resolved, the server's response is passed to the first callback function. The second callback function is called with the parsed data from the response.

3. Promises

Promises are a more recent JavaScript addition that provides a cleaner syntax for asynchronous programming. A promise is an object that represents the eventual completion (or failure) of an asynchronous operation and allows multiple asynchronous operations to be chained together.

Here’s an example of a Promises

function wait(delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, delay);
});
}

console.log('Before wait() 1');
wait(2000).then(() => {
console.log('After wait()');
});
console.log('Before wait() 2');

In this example, thewait() function returns a new Promise object that resolves after the setTimeout()function specifies a delay. The returned promise is then passed to the.then() method, which registers a callback function that is executed once the promise is resolved.

When you run this code, you will get the following results:

Before wait() 1
Before wait() 2
After wait()

A Promise-like object using the .then() method, which allows it to be treated as a Promise for asynchronous programming purposes.

function wait(delay) {
return {
then: function(callback) {
setTimeout(callback, delay);
}
};
}

console.log('Before wait() 1');
wait(2000).then(function() {
console.log('After wait()');
});
console.log('Before wait() 2');

Thewait() function in this example returns an object with athen() method, similar to a standard Promise. When the method then()is called with a callback function, it sets a timer and schedules the callback to run when the timer expires.

The method then()returns the same object, allowing for the chaining of multiple asynchronous operations in a fluent and readable manner.

When this code is executed, the following output will be produced, as shown above:

Before wait() 1
Before wait() 2
After wait()

While this example does not explicitly use the Promise keyword, it demonstrates how the concept of Promises can be implemented using a simple object with a method such as then() and highlights the Promises’ flexibility.

4. Async/await

Async/await is a more recent JavaScript addition that provides a cleaner and more concise syntax for asynchronous programming. Async/await is built on Promises and allows you to write asynchronous code that appears to be synchronous.

async function fetchData() {
try {
const response = await fetch('https://example.com/data');
const data = await response.json();
// Do something with the data
} catch (error) {
// Handle errors
}
}

The fetchData() function in this example is declared as 'async,' which means it returns a Promise. The 'await' keyword is used within the function to wait for the server's response and the parsed data. The try/catch block is used to handle any errors that may occur.

JavaScript Asynchronous Programming Examples

Many areas of web development use asynchronous programming, such as retrieving data from a server, animating a web page, performing complex calculations, and so on.

  1. Fetching Data from a Server
  2. Animating a web page
  3. Running Long Calculations

Fetching Data

A common use case for asynchronous programming in JavaScript is data retrieval from a server. Here’s an example of using the fetch() function to fetch data from a server and display it on a web page

fetch('https://example.com/data')
.then(response => response.json())
.then(data => {
//You can pass the data to a state or a variable for later use
console.log(data)
});

In this example, the fetch() is used to fetch data from the server. The first callback function is used to parse the server’s response as JSON, and the second callback function is used to display the data on the web page.

Animating a Web Page

Animating a web page is another common use case for asynchronous programming in JavaScript.

Here’s an example of using the requestAnimationFrame()function to animate a web page:

function animate() {
const element = document.getElementById('box');
let position = 0;

function frame() {
position++;
element.style.left = position + 'px';
requestAnimationFrame(frame);
}

requestAnimationFrame(frame);
}

In this example, animate() the function is called to animate an HTML element with the ID "box." Theframe() function is used to update the element’s position on each animation frame and therequestAnimationFrame() is used to schedule the next animation frame.

Running Long Calculations

A third common use case for asynchronous programming in JavaScript is running long calculations.

Here's an example of how to use the Web Worker API to perform a long calculation in a separate thread:

const worker = new Worker('worker.js');

worker.postMessage({ value: 42 });

worker.onmessage = event => {
const result = event.data;
// Do something with the result
};

In this example, a web worker is created to run a long calculation in a separate thread. ThepostMessage() method is used to send a value to the worker, and theonmessage event is used to receive the result from the worker.

Asynchronous programming best practices in JavaScript

There are a few best practices to follow when working with asynchronous programming in JavaScript to make your code more reliable and easier to maintain.

  • Avoid Callback Hell

A common pitfall of using callbacks for asynchronous programming is callback hell. It happens when there are nested callbacks that are difficult to read and maintain.

Consider using promises or async/await instead of callbacks to avoid callback hell. Promises and async/await provide a more streamlined and concise syntax for dealing with asynchronous code.

  • Error Handling

Another important aspect of working with asynchronous programming in JavaScript is error handling. When an error occurs, it is critical to handle it gracefully and provide useful feedback to the user.

To handle errors when working with promises or async/await, use the '.catch()' method. To handle errors when working with callbacks, use the error parameter.

  • Debugging Asynchronous Code

Because asynchronous code is executed out of order, debugging it can be difficult. Consider using the console.log() function to output information about the state of your code to aid in debugging.

You can also use browser developer tools to debug your asynchronous code. Chrome DevTools, Firefox Developer Tools, Safari Web Inspector, and other tools are available in most modern browsers.

Summary of Key Points:

Asynchronous programming is a key technique in modern web development that enables developers to create more responsive and efficient applications. The article covered the following topics:

  • Asynchronous programming differs from synchronous programming in that it allows multiple tasks to be performed simultaneously, improving performance and responsiveness.
  • Callbacks, Promises, and async/await are common techniques for working with asynchronous code in JavaScript, each with its strengths and weaknesses.
  • Examples of using asynchronous programming in web development include fetching data from a server, animating a web page, and running long calculations.
  • Best practices for working with asynchronous programming, include avoiding callback hell, handling errors gracefully, and debugging asynchronous code using browser developer tools.
  • To further their understanding of asynchronous programming in JavaScript, beginners can explore advanced topics such as generators, observables, and reactive programming, as well as best practices for handling errors and debugging asynchronous code.

By following these best practices and experimenting with different techniques for working with asynchronous code, you can become a more proficient JavaScript developer and build more responsive and efficient web applications.

Conclusion

In conclusion, asynchronous programming is a powerful technique that allows developers to write more responsive and efficient web applications. However, it can be challenging for beginners to understand the key concepts and best practices for working with asynchronous code in JavaScript. I hope this article will provide a soft landing for learning and understanding asynchronous programming in JavaScript.

Thank you for reading.

--

--

Oladipupo Ishola

Tech Writer | Mentor & Code Instructor | Full Stack Developer | Frontend Developer | Backend Developer | MLH ’21 Alum