The Event Loop in JavaScript
The Event Loop in JavaScript
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Before we get to callbacks, promises, and async/await, we need to understand HOW JavaScript handles asynchrony at all – despite a single, SINGLE-THREADED execution model.
JavaScript is single-threaded
JavaScript executes EXACTLY ONE line of code at any given moment – unlike languages with "real" multithreading, there are no threads running in parallel within a single JavaScript program. This raises a fair question: how can JavaScript then handle network requests, timers, and file reads WITHOUT blocking the entire program?
The call stack
The call stack tracks which function is currently running and which other function called it – EXACTLY the mechanism that, without a base case, eventually overflows in the recursion from chapter 18 ("Maximum call stack size exceeded").
function calculateTax(amount) {
return amount * 0.19;
}
function formatAmount(amount) {
const tax = calculateTax(amount); // Call stack: [formatAmount, calculateTax]
return `${amount} EUR (tax: ${tax} EUR)`;
}
console.log(formatAmount(100)); // Call stack: [console.log, formatAmount]Browser/Node.js APIs: offloading outside the engine
Time-consuming operations like setTimeout, network requests, or file reads are NOT run by the JavaScript engine itself – they get DELEGATED to the surrounding runtime (browser or Node.js), which handles that work OUTSIDE the call stack in the background, while JavaScript itself keeps running the rest of the code.
The callback queue and the event loop
Once an offloaded operation finishes (e.g. the setTimeout delay has elapsed), its callback function is enqueued into a callback queue (also called a "task queue"). The event loop has EXACTLY one job: it PERMANENTLY checks whether the call stack is EMPTY – and, once it is, pushes the next waiting callback function from the queue onto the stack.
console.log('1: Start'); // runs immediately, synchronously
setTimeout(() => {
console.log('3: Timeout callback'); // only runs once the call stack is EMPTY
}, 0); // even with a 0ms delay!
console.log('2: End'); // runs immediately, synchronously
// Actual output order:
// 1: Start
// 2: End
// 3: Timeout callbackAchtung: Even setTimeout(fn, 0) does NOT run immediately! The callback only runs after ALL synchronous code has finished, since the event loop only kicks in once the call stack is COMPLETELY empty. This "1, 2, 3" order – not "1, 3, 2" – catches virtually every JavaScript beginner off guard the first time.
The microtask queue: promises take priority
There are actually TWO queues: the "regular" callback queue (for setTimeout and similar) and a microtask queue (for promises, chapter 24). The microtask queue gets COMPLETELY drained after EVERY single block of synchronous code, BEFORE the event loop processes even ONE single callback queue task:
console.log('1: Start');
setTimeout(() => console.log('4: Timeout'), 0);
Promise.resolve().then(() => console.log('3: Promise'));
console.log('2: End');
// Actual output order:
// 1: Start
// 2: End
// 3: Promise <- microtask BEFORE timeout, even though both were registered AFTER the sync code!
// 4: TimeoutTipp: This ordering explains why async/await (chapter 25, which is built on promises under the hood) often feels more "responsive" in practice than setTimeout-based code – promise callbacks are ALWAYS processed preferentially.
Why this matters for our budget app
In chapters 25/26, we'll load transactions ASYNCHRONOUSLY from a file – with this foundational knowledge, we'll then understand EXACTLY why the code "pauses" at certain points and WHEN which code actually runs.