Skip to main content

Synchronous and asynchronous callback

A synchronous callback runs immediately during the call to the main function, in the same call stack, while an asynchronous one runs later, after the current code has finished, through the event loop. The difference is not in the callback syntax but in when the function that received it decides to call it.

Theory

TL;DR

  • A synchronous callback runs immediately, before the current function finishes, in the same call stack.
  • An asynchronous callback is queued and runs once the stack is empty.
  • Synchronous examples: array.map(fn), forEach, sort, JSON.parse with a reviver.
  • Asynchronous examples: setTimeout(fn, 0), fetch().then(fn), event handlers.
  • setTimeout(fn, 0) does not mean "now", it means "as soon as possible, but after all synchronous code".

Quick example

javascript
function process(callback) { console.log('Before the callback'); callback(); // runs immediately console.log('After the callback'); } process(() => console.log('Synchronous callback'));

Output:

text
Before the callback Synchronous callback After the callback

Everything runs in order, with no delays.

The synchronous callback

A synchronous callback runs at once, during the call to the main function, that is in the same call stack and before the current function has returned. It blocks further execution: the next line does not start until the callback is done.

javascript
const numbers = [1, 2, 3]; const doubled = numbers.map(n => { console.log('processing', n); // runs for every element, right now return n * 2; }); console.log(doubled); // [2, 4, 6] is already available on this line

That is exactly why the result of map is available right after the call: the callback is not scheduled, it is simply called.

The asynchronous callback

An asynchronous callback is invoked later, after the current code has finished: on a timer, after a request, after an event. It goes into a task queue, and the event loop picks it up only when the call stack is empty.

javascript
function processAsync(callback) { console.log('Before the callback'); setTimeout(callback, 0); // will run later console.log('After the callback'); } processAsync(() => console.log('Asynchronous callback'));

Output:

text
Before the callback After the callback Asynchronous callback

The callback does not run immediately, it runs once the call stack has been freed, even when the delay is zero.

Side by side

TypeWhen it runsExample
SynchronousImmediately, during the callarray.map(fn)
AsynchronousLater, after the main codesetTimeout(fn, 0), fetch().then(fn)

The microtask and macrotask queues

Asynchronous callbacks have an order of their own too. Promise callbacks land in the microtask queue, which the event loop drains right after the current synchronous code, while setTimeout creates a macrotask and waits for the next iteration of the loop.

javascript
console.log('1 sync'); setTimeout(() => console.log('4 macrotask'), 0); Promise.resolve().then(() => console.log('3 microtask')); console.log('2 sync'); // order: 1 sync, 2 sync, 3 microtask, 4 macrotask

Common mistakes

  • Reading the result of an asynchronous operation right after the function call: it is still undefined, because the callback has not run yet.
  • Believing that setTimeout(fn, 0) runs the function immediately: it waits for an empty call stack.
  • Catching an error from an asynchronous callback with an outer try/catch: by the time the callback runs, that block has already finished.
  • Writing return inside a forEach callback and expecting it to return a value from the enclosing function: forEach ignores the result.
  • Blocking the thread with a heavy synchronous callback in a loop: the page stops responding because the stack is never freed.
  • Confusing the microtask and macrotask queues: Promise.then always wins over setTimeout(..., 0).

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.