Asynchronous Programming
Async Function
JavaScript
Concurrency
Debugging

Why is this async function not behaving asynchronously?

Master System Design with Codemia

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

Introduction

An async function in JavaScript does not automatically make its contents run in parallel or on a background thread. The async keyword only means the function returns a Promise and allows using await inside it. If the function body contains synchronous (CPU-bound) code, that code blocks the event loop just like any other synchronous code. The most common reasons an async function appears to run synchronously are: no await on an asynchronous operation, CPU-bound work blocking the thread, awaiting Promises sequentially instead of concurrently, and calling the async function without handling the returned Promise.

The Synchronous Trap

javascript
1async function fetchData() {
2    // This is synchronous — no await, no I/O
3    const result = heavyComputation();  // Blocks the event loop
4    return result;
5}
6
7// Calling it does NOT make heavyComputation() async
8fetchData().then(console.log);
9console.log("This waits for heavyComputation to finish");

Wrapping synchronous code in async does not move it to another thread. JavaScript is single-threaded — async only helps when you await actual asynchronous operations (network requests, timers, file I/O).

Reason 1: Missing await

javascript
1async function getUser() {
2    // BUG: fetch returns a Promise, but we don't await it
3    const response = fetch('/api/user');  // Missing await
4    const data = response.json();  // TypeError: response.json is not a function
5    return data;
6}
7
8// Fix: add await
9async function getUserFixed() {
10    const response = await fetch('/api/user');
11    const data = await response.json();
12    return data;
13}

Without await, fetch returns a pending Promise object instead of the resolved Response. The code continues immediately with the Promise object, not the actual data.

Reason 2: Sequential awaits Instead of Concurrent

javascript
1// SLOW: each request waits for the previous one
2async function getAll() {
3    const users = await fetch('/api/users').then(r => r.json());     // 500ms
4    const posts = await fetch('/api/posts').then(r => r.json());     // 500ms
5    const comments = await fetch('/api/comments').then(r => r.json()); // 500ms
6    return { users, posts, comments };  // Total: ~1500ms
7}
8
9// FAST: all requests run concurrently
10async function getAllFast() {
11    const [users, posts, comments] = await Promise.all([
12        fetch('/api/users').then(r => r.json()),
13        fetch('/api/posts').then(r => r.json()),
14        fetch('/api/comments').then(r => r.json()),
15    ]);
16    return { users, posts, comments };  // Total: ~500ms
17}

Each await pauses execution until the Promise resolves. If the operations are independent, use Promise.all to run them concurrently.

Reason 3: CPU-Bound Work Blocking the Event Loop

javascript
1async function processImage(data) {
2    // This is synchronous CPU work — async doesn't help
3    for (let i = 0; i < data.length; i++) {
4        data[i] = applyFilter(data[i]);  // Blocks the event loop
5    }
6    return data;
7}
8
9// Fix: offload to a Worker thread (Node.js)
10const { Worker } = require('worker_threads');
11
12function processImageAsync(data) {
13    return new Promise((resolve, reject) => {
14        const worker = new Worker('./image-processor.js', {
15            workerData: data,
16        });
17        worker.on('message', resolve);
18        worker.on('error', reject);
19    });
20}

async/await is for I/O-bound operations. CPU-bound work needs Web Workers (browser) or Worker Threads (Node.js) to avoid blocking.

Reason 4: Not Handling the Returned Promise

javascript
1async function saveData() {
2    await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
3    console.log('Saved');
4}
5
6// BUG: fire-and-forget — errors are silently swallowed
7saveData();
8console.log('This runs before saveData completes');
9
10// Fix: await the call or handle the Promise
11await saveData();
12console.log('This runs after saveData completes');
13
14// Or handle errors explicitly
15saveData().catch(err => console.error('Save failed:', err));

Calling an async function without await or .then() starts the operation but does not wait for it. Errors thrown inside become unhandled Promise rejections.

Reason 5: forEach Does Not Await

javascript
1// BUG: forEach ignores the returned Promises
2async function processItems(items) {
3    items.forEach(async (item) => {
4        await saveItem(item);  // Each iteration starts but is not awaited
5    });
6    console.log('Done');  // Runs before any saveItem completes
7}
8
9// Fix: use for...of for sequential processing
10async function processItemsFixed(items) {
11    for (const item of items) {
12        await saveItem(item);
13    }
14    console.log('Done');  // Runs after all items are saved
15}
16
17// Fix: use Promise.all for concurrent processing
18async function processItemsConcurrent(items) {
19    await Promise.all(items.map(item => saveItem(item)));
20    console.log('Done');
21}

Array.forEach does not handle async callbacks — it calls each function but does not await the returned Promises. Use for...of or Promise.all(items.map(...)) instead.

Debugging Async Issues

javascript
1async function debugExample() {
2    console.time('total');
3
4    console.time('step1');
5    const a = await fetchA();
6    console.timeEnd('step1');  // Shows how long fetchA took
7
8    console.time('step2');
9    const b = await fetchB();
10    console.timeEnd('step2');
11
12    console.timeEnd('total');  // If total ≈ step1 + step2, they ran sequentially
13}

Use console.time to measure whether operations run sequentially or concurrently. If the total time equals the sum of individual times, you have sequential awaits that could be parallelized.

Common Pitfalls

  • Assuming async means parallel: async does not create threads. It enables non-blocking I/O through the event loop. CPU-bound code blocks the thread regardless of async.
  • Using forEach with async callbacks: forEach fires all callbacks but does not await them. The loop completes immediately while the async operations are still running.
  • Sequential awaits for independent operations: Awaiting three independent API calls sequentially takes 3x as long as running them concurrently with Promise.all.
  • Forgetting to await the async function call: asyncFn() without await returns a Promise but does not wait for completion. Errors inside become unhandled rejections.
  • Wrapping synchronous code in async: Adding async to a function that does only synchronous work adds Promise overhead without any benefit. Only use async when the function actually awaits something.

Summary

  • async does not make code run in parallel — it allows await for I/O-bound Promises
  • Always await asynchronous operations (fetch, file reads, database queries)
  • Use Promise.all for independent operations instead of sequential await
  • Never use forEach with async callbacks — use for...of or Promise.all(map(...))
  • CPU-bound work needs Worker Threads, not async/await
  • Always handle the Promise returned by an async function with await, .then(), or .catch()

Course illustration
Course illustration

All Rights Reserved.