Understanding Async JavaScript

JavaScript Async Intermediate | 15 min read

JavaScript is single-threaded, meaning it can only execute one task at a time. But thanks to its asynchronous nature, it can handle multiple operations concurrently without blocking the main thread. Let's explore how this works.

πŸ”₯ The Problem: Blocking Code

Without async operations, JavaScript would freeze while waiting for slow tasks:

// Synchronous - blocks execution console.log('Start'); const result = fetchDataFromServer(); // Takes 3 seconds console.log(result); // Wait... wait... wait... console.log('End'); // Only runs after fetch completes

⚑ The Solution: Event Loop

JavaScript uses an event loop to handle async operations. Here's a visual representation:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CALL STACK β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚main() β”‚ ← Currently executing β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ WEB APIs (Browser) β”‚ β”‚ β€’ setTimeout() β€’ fetch() β€’ DOM events β”‚ β”‚ β€’ XMLHttpRequest β€’ console.log β€’ WebSocket β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CALLBACK QUEUE β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚task 1 β”‚β†’β”‚task 2 β”‚β†’β”‚task 3 β”‚ (FIFO) β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EVENT LOOP β”‚ ← Watches call stack β”‚ "Is stack β”‚ & moves tasks when β”‚ empty?" β”‚ empty β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“ Callbacks: The Foundation

The oldest pattern for handling async operations:

function fetchData(callback) { setTimeout(() => { const data = { id: 1, name: 'Alice' }; callback(data); }, 1000); } fetchData((result) => { console.log(result); // { id: 1, name: 'Alice' } });
Callback Hell: Nesting multiple callbacks creates unreadable, pyramid-shaped code:
getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { getMoreData(c, function(d) { // 😱 Deep nesting! }); }); }); });

🎯 Promises: A Better Way

Promises represent a value that may not exist yet but will be resolved at some point:

const fetchUser = new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve({ id: 1, name: 'Bob' }); } else { reject(new Error('Failed to fetch')); } }, 1000); }); // Using the promise fetchUser .then(user => console.log(user)) .catch(error => console.error(error)) .finally(() => console.log('Done'));

✨ Async/Await: Modern Syntax

Syntactic sugar over promises that makes async code look synchronous:

// Function marked as async async function getUserData() { try { const response = await fetch('/api/user'); const user = await response.json(); console.log(user); return user; } catch (error) { console.error('Error:', error); throw error; } } // Calling async function getUserData().then(user => { console.log('Got user:', user); });
Key Points:
  • async makes a function return a Promise
  • await pauses execution until Promise resolves
  • Use try/catch for error handling
  • Can only use await inside async functions

πŸ”„ Promise.all: Parallel Execution

Run multiple async operations concurrently:

async function loadDashboard() { // These run in parallel! const [user, posts, notifications] = await Promise.all([ fetch('/api/user').then(r => r.json()), fetch('/api/posts').then(r => r.json()), fetch('/api/notifications').then(r => r.json()) ]); return { user, posts, notifications }; } // vs sequential (slower): async function loadDashboardSlow() { const user = await fetch('/api/user').then(r => r.json()); // 100ms const posts = await fetch('/api/posts').then(r => r.json()); // 100ms const notifications = await fetch('/api/notifications')...; // 100ms // Total: 300ms }

⚠️ Common Pitfalls

1. Forgetting await

async function getData() { const promise = fetch('/api/data'); // Returns Promise console.log(promise); // Promise { } const response = await promise; // βœ… Correct const data = await response.json(); }

2. Looping with async

// ❌ Wrong: forEach doesn't wait async function processItems(items) { items.forEach(async (item) => { await saveToDatabase(item); // These run concurrently! }); console.log('Done'); // Prints before saves complete } // βœ… Correct: Use for...of async function processItems(items) { for (const item of items) { await saveToDatabase(item); // Waits for each one } console.log('Done'); // Prints after all saves }

3. Unhandled Promise Rejections

// ❌ Missing error handling fetch('/api/data') .then(r => r.json()) .then(data => console.log(data)); // If fetch fails, unhandled rejection! // βœ… Always handle errors fetch('/api/data') .then(r => r.json()) .then(data => console.log(data)) .catch(err => console.error('Failed:', err));

πŸŽ“ Quick Reference

Pattern Use When
Callbacks Legacy code, simple one-time operations
Promises Chaining, multiple async operations
Async/Await Modern code, readable async flow
Promise.all() Running multiple operations in parallel

πŸ“š Practice Exercise

Challenge: Rewrite this callback-based code using async/await:

function getUser(callback) { setTimeout(() => callback({ id: 1 }), 100); } function getPosts(userId, callback) { setTimeout(() => callback(['Post 1', 'Post 2']), 100); } getUser((user) => { getPosts(user.id, (posts) => { console.log(posts); }); });

Solution: Check the console for hints, or visit our cheatsheet.

πŸ”— Related Articles

← Back to Academy