JavaScriptSynchronous vs Asynchronous

Synchronous vs Asynchronous

Most modern JavaScript work happens around slow things — network requests, file I/O, timers, user clicks. The language is single-threaded, so the only way it can do many of those at once is to not block while it waits. This page is about what "blocking" means, when it happens, and why the asynchronous APIs you will use every day are designed the way they are.

What "synchronous" means

Synchronous code runs top to bottom on the single call stack. Each line completes — or throws — before the next one starts. While a synchronous call is running, nothing else on the page can.

JS
function add(a, b) {
  return a + b;
}

console.log("before");
const result = add(2, 3);
console.log("after", result);
before
after 5

That is the simple case: everything runs in the order you wrote it. Most of your code looks like this and that is fine — addition is fast.

What blocks the thread?

Anything synchronous that takes a long time blocks the thread. While the call stack is busy, the page cannot:

  • Repaint or animate. Frames freeze, scrolling stutters.

  • Process user input — clicks, taps, key presses queue up.

  • Run any other JavaScript, including timers and promise callbacks.

A blocking loop

JS
function busyWait(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) { /* spin */ }
}

console.log("start");
busyWait(2000);          // page freezes for 2s
console.log("done");
Sneaky blocking
Some browser APIs are synchronous and very slow. `alert`, `confirm`, `prompt`, `localStorage` access on large values, big `JSON.parse`, image decoding done on the main thread — they all freeze the UI while they run.
What "asynchronous" means

Asynchronous code says start this slow thing now, run something else later when it is ready. The slow thing is handed to the host environment (the browser or Node), which does the waiting outside the JavaScript thread. When it finishes, your callback is queued to run as soon as the call stack is free.

JS
console.log("before");

setTimeout(() => {
  console.log("two seconds later");
}, 2000);

console.log("after");
before
after
two seconds later   (about 2s later)

The timer does not block. The engine kept executing the top-level code and only ran the callback after the script had finished and the 2s elapsed.

The three flavours of async API

Asynchronous APIs in JavaScript come in three historical layers. You will see all of them in real code.

  • Callbacks — pass a function that will be called "later". setTimeout, fs.readFile, event listeners. Simple, but nesting many of them produces callback hell.

  • Promises — an object that represents a future value. You chain .then instead of nesting callbacks.

  • async / await — syntax that lets you write code that looks synchronous on top of promises. Linear flow, normal try / catch, easy to read.

The same operation, three styles

JS
// Callback style
function getUser(id, cb) {
  setTimeout(() => cb(null, { id, name: "Ada" }), 100);
}
getUser(1, (err, user) => console.log(user));

// Promise style
function getUserP(id) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ id, name: "Ada" }), 100)
  );
}
getUserP(1).then((user) => console.log(user));

// async / await style
async function main() {
  const user = await getUserP(1);
  console.log(user);
}
main();
A mental model

The clearest way to think about this:

  • Your JavaScript runs on a single thread with one call stack.

  • When you call an async API, you hand the slow work to the host and immediately get back control.

  • When the slow work finishes, the host schedules your callback on a queue.

  • The event loop keeps watching the stack. When the stack is empty, it pulls the next callback from the queue and runs it.

Why this model?
A single thread is much easier to reason about — no locks, no data races on shared memory. The cost is that any long synchronous task freezes everything, so async APIs are the rule rather than the exception.
Recognising async code in the wild

Most of the time you can tell something is async because it returns a Promise or it takes a callback that runs "later".

JS
// Returns a Promise — async.
const p = fetch("/api/users");

// Takes a callback that fires later — async.
document.addEventListener("click", () => console.log("clicked"));

// Returns the value immediately — synchronous.
const len = "hello".length;

// Looks like a function call but the disk read happens later in Node:
import { readFile } from "node:fs/promises";
const data = await readFile("notes.txt", "utf8");
Rules of thumb
  • Keep synchronous work short. A rough budget is ~16 ms per frame for smooth animation.

  • For heavy computation, move it to a Web Worker so the main thread stays responsive.

  • Prefer async APIs (fetch, fs/promises, await) over their blocking siblings (XMLHttpRequest in sync mode, fs.readFileSync).

  • Async does not mean parallel. Two awaited calls run sequentially unless you start them together with Promise.all.

Don't await for the sake of it
`await` makes async code look sync, but every `await` is a pause point. If two independent requests do not depend on each other, fire them off first and `await` the results together — see the Promise combinators page.