Asynchronous Programming in JavaScript: A Complete Practical Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Asynchronous Programming in JavaScript: From Callbacks to async/await



JavaScript is a single-threaded language, but its execution model allows it to efficiently handle thousands of simultaneous operations without blocking the main thread. This is possible thanks to asynchronous programming. In Node.js, where I/O operations (reading files, database queries, network calls) take up 90% of the time, understanding asynchronicity becomes a critical skill. In this guide, we will break down three evolutionary stages of working with asynchronicity: callbacks, promises, and async/await, and also take a look under the hood of the Event Loop.



1. Callbacks: The Foundation of Asynchronicity



A callback is a function that is passed as an argument to another function and is called after the completion of an asynchronous operation. This is the oldest and most basic pattern in JavaScript. In Node.js, many built-in modules (fs, http) use callbacks.



Example of reading a file with a callback:



const fs = require('fs');

console.log('Starting file read...');

fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('Error:', err); return; } console.log('File content:', data);});

console.log('Code continues executing...');


The console output will show that "Code continues executing..." appears before the file content. This is the non-blocking behavior: the main thread does not wait for the read to complete but continues execution.



The Problem with Callbacks: Callback Hell

With nested asynchronous operations, the code turns into a "pyramid of doom":



fs.readFile('user.json', (err, userData) => {  if (err) throw err;  const user = JSON.parse(userData);  db.query(`SELECT * FROM posts WHERE user_id=${user.id}`, (err, posts) => {    if (err) throw err;    fs.writeFile('user_posts.json', JSON.stringify(posts), (err) => {      if (err) throw err;      console.log('Done!');    });  });});


Such code is difficult to read, debug, and maintain. The solution is promises.



2. Promises: Chains and Error Handling



A Promise is an object representing the future result of an asynchronous operation. It can be in one of three states: pending, fulfilled, or rejected.



Creating a simple promise:



const readFilePromise = (filename) => {  return new Promise((resolve, reject) => {    fs.readFile(filename, 'utf8', (err, data) => {      if (err) {        reject(err);      } else {        resolve(data);      }    });  });};

readFilePromise('example.txt') .then(data => console.log('Data:', data)) .catch(err => console.error('Error:', err));


Promise Chaining

The main advantage is the ability to chain sequential operations without nesting:



readFilePromise('user.json')  .then(userData => {    const user = JSON.parse(userData);    return dbQueryPromise(`SELECT * FROM posts WHERE user_id=${user.id}`);  })  .then(posts => {    return writeFilePromise('user_posts.json', JSON.stringify(posts));  })  .then(() => console.log('Done!'))  .catch(err => console.error('Error in chain:', err));


Parallel Execution: Promise.all

When you need to execute several independent operations simultaneously:



const promise1 = readFilePromise('file1.txt');const promise2 = readFilePromise('file2.txt');const promise3 = readFilePromise('file3.txt');

Promise.all([promise1, promise2, promise3]) .then(results => { console.log('All files read:', results); }) .catch(err => console.error('One of the promises failed:', err));


Promise.all completes successfully only if all promises are fulfilled. If at least one fails, it triggers

Blogs

Book Recommendations