JavaScript Asynchronous Programming

JavaScript is a single-threaded language, meaning it can only execute one piece of code at a time. Asynchronous programming allows JavaScript to perform long-running tasks (like fetching data) without blocking the main thread.

Synchronous vs Asynchronous

In synchronous code, each statement runs one after another, waiting for the previous one to complete. In asynchronous code, a task can be started and the program continues executing while waiting for the task to finish.

Synchronous Code
// Synchronous - runs in order
console.log('Step 1');
console.log('Step 2');
console.log('Step 3');
// Output: Step 1, Step 2, Step 3
Asynchronous Code
// Asynchronous - setTimeout delays execution
console.log('Step 1');
setTimeout(function() {
  console.log('Step 2 (delayed)');
}, 0);
console.log('Step 3');
// Output: Step 1, Step 3, Step 2 (delayed)

Why Async Matters

Without asynchronous programming, the browser would freeze during long operations like network requests, file reading, or timers. Async allows the UI to remain responsive while these operations complete in the background.

Blocking vs Non-Blocking
// Simulating a non-blocking operation
console.log('Requesting data...');

setTimeout(function() {
  console.log('Data received!');
}, 2000);

console.log('UI is still responsive!');
console.log('User can keep interacting...');

The Event Loop

The event loop is the mechanism that allows JavaScript to perform non-blocking operations. It continuously checks if the call stack is empty and moves tasks from the callback queue to the call stack for execution.

Event Loop in Action
console.log('1. Script start');

setTimeout(function() {
  console.log('2. setTimeout callback');
}, 0);

Promise.resolve().then(function() {
  console.log('3. Promise callback');
});

console.log('4. Script end');
// Output order: 1, 4, 3, 2
// Promise (microtask) runs before setTimeout (macrotask)

Call Stack, Callback Queue, and Microtask Queue

The JavaScript engine uses several components to manage code execution. The call stack executes functions, the callback queue (macrotask queue) holds callbacks from setTimeout, events, etc., and the microtask queue holds promise callbacks and queueMicrotask.

ComponentPurposePriority
Call StackExecutes functions in LIFO orderImmediate
Microtask QueuePromise .then(), queueMicrotask()High (runs first)
Callback QueuesetTimeout, setInterval, eventsLower (runs after microtasks)
Event LoopMoves tasks from queues to stackCoordinator
Microtasks vs Macrotasks
console.log('Start');

// Macrotask (callback queue)
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);

// Microtask (microtask queue)
Promise.resolve().then(() => console.log('Promise 1'));
Promise.resolve().then(() => console.log('Promise 2'));

console.log('End');
// Output: Start, End, Promise 1, Promise 2, Timeout 1, Timeout 2
📝 Note: Microtasks (Promises) always execute before macrotasks (setTimeout/setInterval) when the call stack is empty. This is why Promise callbacks run before setTimeout callbacks even if setTimeout has a delay of 0 milliseconds.
Exercise:
In the event loop, which queue has higher priority?
Try it YourselfCtrl+Enter to run
Click Run to see the output here.