JavaScript: Asynchronous JavaScript Basics

Understand how JavaScript handles time-consuming operations without blocking the page using the event loop, callbacks, and timers.

Synchronous vs Asynchronous Code

Synchronous code runs line by line: each line must finish before the next begins. Asynchronous code lets certain operations run in the background and continue the rest of the program without waiting.

Sync vs Async

Asynchronous programming is essential for anything that involves waiting: network requests, timers, file reads.

  • Synchronous: each statement blocks until it finishes before moving on
  • Asynchronous: long-running work is offloaded; a callback runs when it is done
  • Without async, a slow network request would freeze the entire page
  • JavaScript uses the event loop to handle async without multiple threads

Sync vs Async Output Order

The async callback runs after the rest of the synchronous code finishes.

JavaScript's Single-Threaded Nature

JavaScript has one call stack and one thread of execution. It can only do one thing at a time. Async tasks are handled by the browser's Web APIs and scheduled back via the event loop: not by running on a second thread.

Single Thread

One thread means no two pieces of JS code ever run at the exact same moment: the event loop creates the illusion of concurrency.

  • Long-running synchronous code (a heavy loop) blocks everything, the page freezes
  • Async APIs (fetch, setTimeout) are handled outside the JS engine by browser Web APIs
  • When a Web API finishes, it pushes the callback into the task queue to be picked up by the event loop
  • This design avoids complex threading bugs like race conditions and deadlocks

Blocking the Thread

A synchronous loop blocks the call stack: no other code can run during it.

Call Stack

The call stack is a data structure that keeps track of where the program is in its execution. When a function is called it is pushed onto the stack; when it returns it is popped off.

Call Stack

The call stack is a LIFO (last-in, first-out) structure: the most recently called function is always the first to finish.

  • Each function call creates a new stack frame containing its local variables and context
  • When the stack is empty, the event loop can push the next queued callback
  • A "Maximum call stack size exceeded" error means infinite or too-deep recursion
  • You can see the current call stack in the browser DevTools Sources panel

Call Stack Trace

Functions are pushed and popped in LIFO order: innermost finishes first.

Callback Functions

A callback is a function passed as an argument to another function, to be called once a task completes. Callbacks are the original async pattern in JavaScript.

Callbacks

Callbacks let you say 'when this finishes, run that' without blocking the rest of the code.

  • Any function can be a callback, it is just a function passed as an argument
  • Used by: setTimeout, addEventListener, array methods like forEach
  • The caller decides when to invoke the callback
  • Synchronous callbacks run immediately (like forEach); async callbacks run later

Callback Function

onSuccess is called once the simulated fetch completes after 500ms.

Callback Hell (Pyramid of Doom)

When multiple async operations depend on each other, callbacks nest inside callbacks, creating deeply indented, hard-to-read code known as callback hell.

Callback Hell

Deeply nested callbacks are hard to read, debug, and maintain, Promises and async/await were created to solve this.

  • Each async step must be nested inside the previous step's callback
  • Error handling requires a check in every single callback
  • Hard to add steps, reorder logic, or refactor
  • Solution: Promises or async/await (covered in the next topic)

Callback Hell Example

Each dependent step adds another level of indentation, the pyramid of doom.

Error-first Callbacks

Node.js popularised the error-first callback convention: the first argument of every callback is an error object (or null if everything went fine), and the second argument is the result.

Error-first Convention

Always check the first argument before using the result skipping this check hides errors silently.

  • Signature: callback(error, result)
  • If error is truthy, something went wrong: handle it and return early
  • If error is null, use the result safely
  • Every async callback in Node.js core APIs follows this pattern (fs, http, etc.)

Error-first Callback

Check err first, return early on failure, then safely use the result.

Timers: setTimeout and setInterval

setTimeout runs a callback once after a delay.setInterval runs a callback repeatedly at a fixed interval. Both return an ID you can use to cancel them.

setTimeout / setInterval

Timer callbacks are async, they never run before the current synchronous code finishes.

  • setTimeout(fn, ms): calls fn once after at least ms milliseconds
  • setInterval(fn, ms): calls fn every ms milliseconds until cleared
  • The delay is a minimum, not a guarantee: a busy call stack delays them further
  • Both return a numeric ID used to cancel with clearTimeout / clearInterval

setTimeout and setInterval

Run code once with a delay or repeatedly on an interval, with cancellation.

clearTimeout and clearInterval

Store the ID returned by a timer and pass it to the matching clear function to cancel it before it fires.

Clearing Timers

Always clear timers you no longer need: runaway intervals cause memory leaks and unexpected behaviour.

  • Store the return value: const id = setTimeout(fn, ms)
  • clearTimeout(id): cancels a pending one-shot timer
  • clearInterval(id): stops a repeating interval permanently
  • In React and other frameworks, always clear timers in the component cleanup (useEffect return)

clearInterval in Action

A second setTimeout clears the interval after 5 seconds.

Web APIs and the Event Loop

When you call setTimeout or fetch, the browser handles the waiting outside the JS engine. Once done, the callback is placed in a queue. The event loop moves it to the call stack as soon as the stack is empty.

Event Loop

The event loop is the bridge between the JS engine and the browser's Web APIs.

  • Web APIs (setTimeout, fetch, DOM events) run outside the JS engine in the browser
  • When a Web API finishes, it pushes the callback into the task queue
  • The event loop checks: if the call stack is empty, move the next queued callback onto it
  • This is why async callbacks never interrupt synchronous code: they wait for the stack to clear

Event Loop Order

Sync code runs first, then microtasks, then task queue callbacks: even with a 0ms delay.

Task Queue and Microtask Queue

There are two queues. The microtask queue (Promises, queueMicrotask) has higher priority and is fully drained after each task, before the next task queue item runs.

Task Queue vs Microtask Queue

Microtasks run between tasks: the queue is emptied completely before any task queue callback executes.

  • Task queue (macrotask queue): setTimeout, setInterval, DOM events, I/O
  • Microtask queue: Promise .then/.catch/.finally, queueMicrotask, MutationObserver
  • After each task, ALL pending microtasks run before the next task starts
  • Stacking too many microtasks recursively can starve the task queue (rare but possible)

Task vs Microtask Priority

Both Promise callbacks run before either setTimeout callback, despite being queued after.

QueueSourcesPriorityWhen it runs
Microtask queuePromise callbacks, queueMicrotaskHigherAfter every task, before the next task
Task queuesetTimeout, setInterval, DOM eventsLowerOne task per event loop iteration

Knowledge Check

1. What does it mean for JavaScript to be single-threaded?

2. What is the call stack?

3. What is "callback hell"?

4. What does setTimeout(fn, 0) guarantee?

5. What does clearInterval() do?

6. What is the event loop responsible for?

7. Which queue has higher priority: the task queue or the microtask queue?