Suggest an editImprove this articleRefine the answer for “callback functions and callback hell in JavaScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Callback function** - a function passed as an argument to another function, called later after some work finishes. ```javascript [1, 2, 3].forEach(n => console.log(n)); // forEach's argument is a callback setTimeout(() => console.log('done'), 1000); // async callback, runs after 1s ``` **Key point:** chaining 3+ async callbacks creates callback hell - a rightward pyramid where error handling repeats at every level. Fix it with Promises or async/await.Shown above the full answer for quick recall.Answer (EN)Image**Callback function** - a function you pass into another function so that function can call it later, usually after finishing some work. ## Theory ### TL;DR - Think of leaving your phone number at a restaurant. They call you when the table is ready instead of making you stand in line. That is a callback. - Callbacks split into two types: sync (`.map()`, `.filter()`) run immediately inside the loop; async (`setTimeout`, `fs.readFile`) run after the current call stack clears. - Callback hell happens when you chain 3+ async callbacks. The code drifts rightward, error handling repeats on every level, and adding one step means adding another layer of indentation. - Decision rule: one async step → callback is fine. Two or more chained steps → use Promises or async/await. ### Quick example ```javascript function fetchUser(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'Alice' }; callback(null, user); // error-first: null = no error }, 1000); } fetchUser(123, (err, user) => { if (err) return console.error(err); console.log(user.name); // "Alice" - prints after 1 second }); console.log('This runs first'); // sync code does not wait ``` The outer `console.log` runs immediately. The callback fires one second later because the [event loop](/questions/event-loop) queues it and runs it only when the call stack is empty. That is the whole mechanism. ### Sync vs async callbacks Not all callbacks are async. `.map()` and `.filter()` call your function synchronously, line by line inside the method. `setTimeout` and `fs.readFile` are different: Node or the browser hands the operation to a background API, and your callback goes into the task queue. The event loop picks it up after the current stack clears. This distinction trips people up in interviews. A classic question: ```javascript setTimeout(() => console.log('A'), 0); console.log('B'); // Output: "B" then "A" // 0ms delay does not mean "run now". It means "queue it". ``` ### Error-first callback pattern Node.js standardized a convention: the first argument of every callback is the error. If there is no error, it is `null`. This makes failure-checking consistent across all async APIs. ```javascript fs.readFile('config.json', 'utf8', (err, data) => { if (err) { console.error('File missing:', err.message); return; // always return early on error } const config = JSON.parse(data); console.log(config); }); ``` The most common bug I see in Node.js code is skipping the `err` check entirely. You get `undefined` in `data` with no hint of what failed. ### Callback hell When each async step needs the result from the previous one, you start nesting. After three levels, the code grows rightward. This is callback hell, sometimes called the pyramid of doom. ```javascript // Real pre-Promise API code looked like this getUser(userId, (err, user) => { if (err) return handleError(err); getOrders(user.id, (err, orders) => { if (err) return handleError(err); getOrderDetails(orders[0].id, (err, details) => { if (err) return handleError(err); getShippingInfo(details.shipId, (err, shipping) => { if (err) return handleError(err); console.log(shipping); }); }); }); }); ``` The problem is not just formatting. Error handling duplicates at every level. Adding a step means another nest. Refactoring any part of this chain is slow and error-prone. ### How to fix callback hell Three approaches work in practice. **Promises** flatten the chain into a readable sequence: ```javascript getUser(userId) .then(user => getOrders(user.id)) .then(orders => getOrderDetails(orders[0].id)) .then(details => getShippingInfo(details.shipId)) .then(shipping => console.log(shipping)) .catch(handleError); // one handler for the whole chain ``` **async/await** makes it look like synchronous code: ```javascript async function getShipping(userId) { const user = await getUser(userId); const orders = await getOrders(user.id); const details = await getOrderDetails(orders[0].id); return getShippingInfo(details.shipId); } ``` **Named functions** work as a quick refactor without switching to Promises. Pull callbacks out of each other: ```javascript function onUser(err, user) { if (err) return handleError(err); getOrders(user.id, onOrders); } function onOrders(err, orders) { if (err) return handleError(err); getOrderDetails(orders[0].id, onDetails); } getUser(userId, onUser); // flat, readable, same behavior ``` ### When to use callbacks - Single async operation like one `addEventListener` or a single `setTimeout`: callback works fine. - Array iteration with `.map()`, `.filter()`, `.reduce()`: sync callbacks, no nesting issue. - Two or more chained async steps: switch to [Promises](/questions/promises-in-javascript) or async/await. - Error-prone async in Node.js: Promises give you one `.catch()` instead of repeating `if (err)` at every level. ### Common mistakes **Ignoring the error parameter:** ```javascript // Wrong - data is undefined when file is missing, no error shown fs.readFile('data.txt', (data) => console.log(data)); // Fix fs.readFile('data.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); }); ``` **Using a variable before the callback runs:** ```javascript let user; fetchUser(1, (err, result) => { user = result; // sets after ~500ms }); console.log(user); // undefined - this line runs before the callback fires ``` If you need the data, use it inside the callback or return a Promise. **Assuming `setTimeout(..., 0)` runs synchronously:** ```javascript setTimeout(() => console.log('first'), 0); console.log('second'); // Output: "second", then "first" ``` Zero milliseconds means "queue it after the stack clears", not "run it now". This appears constantly in interviews. **Recursive polling without a stop condition:** ```javascript // Runs forever function poll(cb) { setTimeout(() => poll(cb), 1000); } // Fix: add a counter function poll(cb, count = 0) { if (count >= 10) return cb(); setTimeout(() => poll(cb, count + 1), 1000); } ``` ### Real-world usage - Node.js/Express route handlers: `app.get('/users', (req, res) => { fs.readFile(..., callback) })` - Browser event listeners: `button.addEventListener('click', handler)` - Array methods in every JS codebase: `.map()`, `.filter()`, `.forEach()` - Legacy jQuery AJAX: `$.get('/api/data', {}, callback)` - still alive in older projects - `setTimeout` and `setInterval` for timers and polling ### Follow-up questions **Q:** What is a callback function? **A:** A function passed as an argument to another function, called later after some work finishes. `[1, 2, 3].forEach(n => console.log(n))` is the simplest example. **Q:** What is callback hell and why does it matter? **A:** Nesting 3+ async callbacks creates a rightward pyramid. Error handling repeats at every level, adding a step means adding a layer of indentation, and the code becomes hard to refactor. **Q:** What is the error-first callback pattern? **A:** A Node.js convention where the first argument is always the error (`null` if none). It standardizes error handling so callers always check the error before touching the data. **Q:** What is the difference between sync and async callbacks? **A:** Sync callbacks like `.map()` run immediately inside the calling function. Async callbacks like `setTimeout` get queued by the event loop and run after the current stack empties. **Q:** How does the event loop handle async callbacks? **A:** Node or the browser hands the async work to a background API. When it finishes, the callback goes into the task queue. The event loop moves it to the call stack only when the stack is empty, which is why `setTimeout(..., 0)` still runs after sync code. **Q:** Why does sync recursion risk a stack overflow but async polling does not? **A:** Sync recursion adds a frame to the call stack on every call. Node caps this around 10k frames. Async polling via `setTimeout` offloads each iteration to the task queue, so the stack never grows. Each poll starts fresh. ## Examples ### Basic async callback ```javascript function fetchUser(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'Alice' }; callback(null, user); // null = no error }, 1000); } fetchUser(123, (err, user) => { if (err) return console.error(err); console.log(user.name); // "Alice" - after 1 second }); ``` ### Express.js route with async file read ```javascript const fs = require('fs'); const express = require('express'); const app = express(); app.get('/user/:id', (req, res) => { fs.readFile(`users/${req.params.id}.json`, 'utf8', (err, data) => { if (err) return res.status(500).json({ error: 'User not found' }); res.json(JSON.parse(data)); // { "name": "Bob" } }); }); // GET /user/1 reads the file async, responds after ~10ms ``` The route handler itself is a callback. The `fs.readFile` callback is a second callback inside it. Two levels is still readable - this is where you draw the line. ### Callback hell vs async/await ```javascript // Callback hell: 3 chained async operations getUser(123, (err, user) => { if (err) return handleError(err); getPosts(user.id, (err, posts) => { if (err) return handleError(err); getComments(posts[0].id, (err, comments) => { if (err) return handleError(err); console.log(comments); }); }); }); // async/await: same logic, no nesting async function loadComments(userId) { const user = await getUser(userId); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); return comments; // one try/catch wraps the whole thing } ``` The behavior is identical. The async/await version has one place for error handling and reads top to bottom like normal code.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.