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:
Blocking example:
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:
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):
Asynchronous JavaScript: Promises
Promises represent a future value — pending, fulfilled, or rejected:
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:
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:
Microtasks (Promises) always execute before macrotasks (setTimeout), even if the setTimeout delay is 0.
Parallel vs Sequential Async
Use Promise.all() when operations are independent. Use sequential await when each operation depends on the previous result.
Promise Utilities
Common Pitfalls
- Forgetting
awaitbefore a Promise:const data = fetchUser(1)assigns the Promise object, not the resolved value. Withoutawait,datais a pending Promise and subsequent code operates on the wrong type. - Using
awaitin a regular (non-async) function:awaitis only valid insideasyncfunctions. Using it outside produces aSyntaxError. In top-level module code (ESM), top-levelawaitis allowed. - Sequential
awaitwhen parallel is possible:await fetchA(); await fetchB();takes the sum of both durations. If they are independent, useawait Promise.all([fetchA(), fetchB()])to run them concurrently. - Swallowing errors by not using
.catch()ortry/catch: Unhandled Promise rejections crash Node.js (since v15) and produce warnings in browsers. Always handle errors with.catch()ortry/catcharoundawait. - 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 Workersorworker_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/awaitis 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/catchor.catch()on Promises

