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.parsewith 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
function process(callback) {
console.log('Before the callback');
callback(); // runs immediately
console.log('After the callback');
}
process(() => console.log('Synchronous callback'));Output:
Before the callback
Synchronous callback
After the callbackEverything 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.
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 lineThat 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.
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:
Before the callback
After the callback
Asynchronous callbackThe callback does not run immediately, it runs once the call stack has been freed, even when the delay is zero.
Side by side
| Type | When it runs | Example |
|---|---|---|
| Synchronous | Immediately, during the call | array.map(fn) |
| Asynchronous | Later, after the main code | setTimeout(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.
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 macrotaskCommon 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
returninside aforEachcallback and expecting it to return a value from the enclosing function:forEachignores 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.thenalways wins oversetTimeout(..., 0).
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.