JavaScript
Asynchronous
Synchronous
Programming
Web Development

Asynchronous/Synchronous Javascript

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

JavaScript is single-threaded — it runs one operation at a time on the main thread. Synchronous code blocks execution until each statement completes. Asynchronous code (callbacks, Promises, async/await) lets the engine start an operation, continue executing other code, and handle the result later when it is ready. Understanding this distinction is fundamental to writing performant JavaScript, especially for I/O operations like network requests, file reads, and timers.

Synchronous JavaScript

Synchronous code runs line by line. Each statement waits for the previous one to finish:

javascript
1console.log("First");
2console.log("Second");
3console.log("Third");
4// Output: First, Second, Third (always in this order)

Blocking example:

javascript
1function heavyCalculation() {
2    let sum = 0;
3    for (let i = 0; i < 1_000_000_000; i++) {
4        sum += i;
5    }
6    return sum;
7}
8
9console.log("Start");
10const result = heavyCalculation();  // Blocks for several seconds
11console.log("Result:", result);
12console.log("End");
13// UI is frozen during heavyCalculation

Synchronous blocking is a problem in browsers because the main thread also handles rendering and user input. A long synchronous operation freezes the page.

Asynchronous JavaScript: Callbacks

The original async pattern — pass a function to be called when the operation completes:

javascript
1console.log("Start");
2
3setTimeout(() => {
4    console.log("Timer fired");
5}, 1000);
6
7console.log("End");
8// Output: Start, End, Timer fired (after 1 second)

setTimeout registers the callback and returns immediately. The engine continues to console.log("End"), then executes the callback when the timer expires.

Callback hell (the problem with nested callbacks):

javascript
1getUser(userId, (user) => {
2    getOrders(user.id, (orders) => {
3        getOrderDetails(orders[0].id, (details) => {
4            console.log(details);  // Deeply nested, hard to read and handle errors
5        });
6    });
7});

Asynchronous JavaScript: Promises

Promises represent a future value — pending, fulfilled, or rejected:

javascript
1function fetchUser(id) {
2    return new Promise((resolve, reject) => {
3        setTimeout(() => {
4            if (id > 0) {
5                resolve({ id, name: "Alice" });
6            } else {
7                reject(new Error("Invalid ID"));
8            }
9        }, 1000);
10    });
11}
12
13fetchUser(1)
14    .then(user => {
15        console.log(user.name);  // "Alice"
16        return fetchOrders(user.id);
17    })
18    .then(orders => {
19        console.log(orders);
20    })
21    .catch(error => {
22        console.error(error.message);
23    });

Promises flatten callback nesting into a .then() chain. Errors propagate to the nearest .catch().

Asynchronous JavaScript: async/await

async/await is syntactic sugar over Promises that makes async code read like synchronous code:

javascript
1async function loadUserData(id) {
2    try {
3        const user = await fetchUser(id);
4        const orders = await fetchOrders(user.id);
5        const details = await getOrderDetails(orders[0].id);
6        console.log(details);
7    } catch (error) {
8        console.error("Failed:", error.message);
9    }
10}
11
12loadUserData(1);

await pauses execution of the async function (not the main thread) until the Promise resolves. Other code continues running during the pause.

The Event Loop

The event loop is the mechanism that enables async behavior in single-threaded JavaScript:

 
1Call Stack → runs synchronous code
2     (when empty)
3Microtask QueuePromise callbacks (.then, await continuations)
4     (when empty)
5Macrotask Queue → setTimeout, setInterval, I/O callbacks
javascript
1console.log("1");
2
3setTimeout(() => console.log("2"), 0);
4
5Promise.resolve().then(() => console.log("3"));
6
7console.log("4");
8
9// Output: 1, 4, 3, 2
10// Synchronous first, then microtasks (Promise), then macrotasks (setTimeout)

Microtasks (Promises) always execute before macrotasks (setTimeout), even if the setTimeout delay is 0.

Parallel vs Sequential Async

javascript
1// Sequential — each awaits the previous (slower)
2async function sequential() {
3    const user = await fetchUser(1);      // 1 second
4    const posts = await fetchPosts(1);    // 1 second
5    return { user, posts };               // Total: 2 seconds
6}
7
8// Parallel — both start at the same time (faster)
9async function parallel() {
10    const [user, posts] = await Promise.all([
11        fetchUser(1),       // 1 second
12        fetchPosts(1),      // 1 second (runs concurrently)
13    ]);
14    return { user, posts }; // Total: 1 second
15}

Use Promise.all() when operations are independent. Use sequential await when each operation depends on the previous result.

Promise Utilities

javascript
1// Promise.all — resolves when ALL resolve, rejects on first rejection
2const results = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
3
4// Promise.allSettled — waits for ALL to complete (never rejects)
5const outcomes = await Promise.allSettled([fetch(url1), fetch(url2)]);
6outcomes.forEach(o => {
7    if (o.status === "fulfilled") console.log(o.value);
8    if (o.status === "rejected") console.log(o.reason);
9});
10
11// Promise.race — resolves/rejects with the first settled promise
12const fastest = await Promise.race([fetch(url1), fetch(url2)]);
13
14// Promise.any — resolves with the first fulfilled (ignores rejections)
15const firstSuccess = await Promise.any([fetch(url1), fetch(url2)]);

Common Pitfalls

  • Forgetting await before a Promise: const data = fetchUser(1) assigns the Promise object, not the resolved value. Without await, data is a pending Promise and subsequent code operates on the wrong type.
  • Using await in a regular (non-async) function: await is only valid inside async functions. Using it outside produces a SyntaxError. In top-level module code (ESM), top-level await is allowed.
  • Sequential await when parallel is possible: await fetchA(); await fetchB(); takes the sum of both durations. If they are independent, use await Promise.all([fetchA(), fetchB()]) to run them concurrently.
  • Swallowing errors by not using .catch() or try/catch: Unhandled Promise rejections crash Node.js (since v15) and produce warnings in browsers. Always handle errors with .catch() or try/catch around await.
  • Blocking the event loop with synchronous code: A CPU-intensive synchronous operation (large loop, synchronous file read) blocks the entire event loop, preventing async callbacks from executing. Use Web Workers or worker_threads (Node.js) for CPU-heavy tasks.

Summary

  • Synchronous code blocks execution — each line waits for the previous one
  • Asynchronous code (callbacks, Promises, async/await) starts operations and handles results later
  • async/await is the modern standard — reads like synchronous code but is non-blocking
  • The event loop processes microtasks (Promises) before macrotasks (setTimeout)
  • Use Promise.all() for parallel execution of independent async operations
  • Always handle errors with try/catch or .catch() on Promises

Course illustration
Course illustration

All Rights Reserved.