docs/concepts/callbacks.mdx
Why doesn't JavaScript wait? When you set a timer, make a network request, or listen for a click, how does your code keep running instead of freezing until that operation completes?
console.log('Before timer')
setTimeout(function() {
console.log('Timer fired!')
}, 1000)
console.log('After timer')
// Output:
// Before timer
// After timer
// Timer fired! (1 second later)
The answer is callbacks: functions you pass to other functions, saying "call me back when you're done." As defined on MDN, a callback function is passed into another function as an argument and is then invoked inside the outer function. Callbacks power everything async in JavaScript. Every event handler, every timer, every network request. They all rely on them.
<Info> **What you'll learn in this guide:** - What callbacks are and why JavaScript uses them - The difference between synchronous and asynchronous callbacks - How callbacks connect to higher-order functions - Common callback patterns (event handlers, timers, array methods) - The error-first callback pattern (Node.js convention) - Callback hell and the "pyramid of doom" - How to escape callback hell - Why Promises were invented to solve callback problems </Info> <Warning> **Prerequisites:** This guide assumes familiarity with [the Event Loop](/concepts/event-loop). It's the mechanism that makes async callbacks work! You should also understand [higher-order functions](/concepts/higher-order-functions), since callbacks are passed to higher-order functions. </Warning>A callback is a function passed as an argument to another function, that gets called later. The other function decides when (or if) to run it.
// greet is a callback function
function greet(name) {
console.log(`Hello, ${name}!`)
}
// processUserInput accepts a callback
function processUserInput(callback) {
const name = 'Alice'
callback(name) // "calling back" the function we received
}
processUserInput(greet) // "Hello, Alice!"
The term "callback" comes from the idea of being called back. Think of it like getting a buzzer at a restaurant: "We'll buzz you when your table is ready."
<Tip> **Here's the thing:** A callback is just a regular function. Nothing magical about it. What makes it a "callback" is *how it's used*: passed to another function to be executed later. </Tip>You don't have to define callbacks as named functions. Anonymous functions (and arrow functions) work just as well:
// Named function as callback
function handleClick() {
console.log('Clicked!')
}
button.addEventListener('click', handleClick)
// Anonymous function as callback
button.addEventListener('click', function() {
console.log('Clicked!')
})
// Arrow function as callback
button.addEventListener('click', () => {
console.log('Clicked!')
})
All three do the same thing. Named functions are easier to debug though, and you can reuse them.
Callbacks work like the buzzer you get at a busy restaurant:
┌─────────────────────────────────────────────────────────────────────────┐
│ THE RESTAURANT BUZZER ANALOGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOU (Your Code) RESTAURANT (JavaScript Runtime) │
│ │
│ ┌──────────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ KITCHEN │ │
│ │ "I'd like │ ────────► │ (Web APIs) │ │
│ │ a burger" │ ORDER │ │ │
│ │ │ │ [setTimeout: 5 min] │ │
│ └──────────────┘ │ [fetch: waiting...] │ │
│ │ │ [click: listening...] │ │
│ │ └─────────────────────────────────┘ │
│ │ │ │
│ │ You get a buzzer │ When ready... │
│ │ and go sit down ▼ │
│ │ ┌─────────────────────────────────┐ │
│ │ │ PICKUP COUNTER │ │
│ ▼ │ (Callback Queue) │ │
│ ┌──────────────┐ │ │ │
│ │ │ │ [Your callback waiting here] │ │
│ │ 📱 BUZZ! │ ◄──────── │ │ │
│ │ │ READY! └─────────────────────────────────┘ │
│ │ Time to │ │
│ │ eat! │ The Event Loop calls your callback │
│ └──────────────┘ when the kitchen (Web API) is done │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The key insight: you don't wait at the counter. You give them a way to reach you (the callback), and you go do other things. That's how JavaScript stays fast — it never sits around waiting. According to the 2023 State of JS survey, while most developers now prefer Promises and async/await for new code, callbacks remain foundational and are still used extensively in event handling and Node.js APIs.
// You place your order (start async operation)
setTimeout(function eatBurger() {
console.log('Eating my burger!') // This is the callback
}, 5000)
// You go sit down (your code continues)
console.log('Sitting down, checking my phone...')
console.log('Chatting with friends...')
console.log('Reading the menu...')
// Output:
// Sitting down, checking my phone...
// Chatting with friends...
// Reading the menu...
// Eating my burger! (5 seconds later)
Callbacks and higher-order functions go hand in hand:
// forEach is a HIGHER-ORDER FUNCTION (it accepts a function)
// The arrow function is the CALLBACK (it's being passed in)
const numbers = [1, 2, 3]
numbers.forEach((num) => { // ← This is the callback
console.log(num * 2)
})
// 2, 4, 6
Every time you use map, filter, forEach, reduce, sort, or find, you're passing callbacks to higher-order functions:
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 17 },
{ name: 'Charlie', age: 30 }
]
// filter accepts a callback that returns true/false
const adults = users.filter(user => user.age >= 18)
// map accepts a callback that transforms each element
const names = users.map(user => user.name)
// find accepts a callback that returns true when found
const bob = users.find(user => user.name === 'Bob')
// sort accepts a callback that compares two elements
const byAge = users.sort((a, b) => a.age - b.age)
Some callbacks run right away. Others run later. Getting this wrong will bite you.
Synchronous callbacks are executed immediately, during the function call. They block until complete.
const numbers = [1, 2, 3, 4, 5]
console.log('Before map')
const doubled = numbers.map(num => {
console.log(`Doubling ${num}`)
return num * 2
})
console.log('After map')
console.log(doubled)
// Output (all synchronous, in order):
// Before map
// Doubling 1
// Doubling 2
// Doubling 3
// Doubling 4
// Doubling 5
// After map
// [2, 4, 6, 8, 10]
The callback runs for each element before map returns. Nothing else happens until it's done.
Common synchronous callbacks:
map, filter, forEach, reduce, find, sort, every, somereplace (with function)Object.keys().forEach()Asynchronous callbacks are executed later, after the current code finishes. They don't block.
console.log('Before setTimeout')
setTimeout(() => {
console.log('Inside setTimeout')
}, 0) // Even with 0ms delay!
console.log('After setTimeout')
// Output:
// Before setTimeout
// After setTimeout
// Inside setTimeout (runs AFTER all sync code)
Even with a 0ms delay, the callback runs after the synchronous code. This is because async callbacks go through the event loop.
Common asynchronous callbacks:
setTimeout, setIntervaladdEventListener, onclickXMLHttpRequest.onload, fetch().then()fs.readFile, http.get| Aspect | Synchronous Callbacks | Asynchronous Callbacks |
|---|---|---|
| When executed | Immediately, during the function call | Later, via the event loop |
| Blocking | Yes — code waits for completion | No — code continues immediately |
| Examples | map, filter, forEach, sort | setTimeout, addEventListener, fetch |
| Use case | Data transformation, iteration | I/O, user interaction, timers |
| Error handling | Regular try/catch works | try/catch won't catch errors! |
| Return value | Can return values | Return values usually ignored |
This trips up almost everyone:
// Synchronous callback - try/catch WORKS
try {
[1, 2, 3].forEach(num => {
if (num === 2) throw new Error('Found 2!')
})
} catch (error) {
console.log('Caught:', error.message) // "Caught: Found 2!"
}
// Asynchronous callback - try/catch DOES NOT WORK!
try {
setTimeout(() => {
throw new Error('Async error!') // This error escapes!
}, 100)
} catch (error) {
// This will NEVER run
console.log('Caught:', error.message)
}
// The error crashes your program!
Why? The try/catch runs immediately. By the time the async callback executes, the try/catch is long gone. The callback runs in a different "turn" of the event loop.
To understand async callbacks, you need to see how they work with the event loop.
┌─────────────────────────────────────────────────────────────────────────┐
│ ASYNC CALLBACK LIFECYCLE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. YOUR CODE RUNS │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ console.log('Start') │ │
│ │ setTimeout(callback, 1000) // Register callback with Web API │ │
│ │ console.log('End') │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. WEB API HANDLES THE ASYNC OPERATION │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Timer starts counting... │ │
│ │ (Your code continues running - it doesn't wait!) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (after 1000ms) │
│ 3. CALLBACK QUEUED │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Timer done! Callback added to Task Queue │ │
│ │ [callback] ← waiting here │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (when call stack is empty) │
│ 4. EVENT LOOP EXECUTES CALLBACK │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Event Loop: "Call stack empty? Let me grab that callback..." │ │
│ │ callback() runs! │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Let's trace through a real example:
console.log('1: Script start')
setTimeout(function first() {
console.log('2: First timeout')
}, 0)
setTimeout(function second() {
console.log('3: Second timeout')
}, 0)
console.log('4: Script end')
Execution order:
console.log('1: Script start') — runs immediately → "1: Script start"setTimeout(first, 0) — registers first callback with Web APIssetTimeout(second, 0) — registers second callback with Web APIsconsole.log('4: Script end') — runs immediately → "4: Script end"firstfirst() runs → "2: First timeout"secondsecond() runs → "3: Second timeout"Output:
1: Script start
4: Script end
2: First timeout
3: Second timeout
Even with a 0ms delay, the callbacks still run after all the synchronous code finishes.
<Tip> **Read more:** Our [Event Loop guide](/concepts/event-loop) goes deep into tasks, microtasks, and rendering. If you want to understand *why* `Promise.then()` runs before `setTimeout(..., 0)`, check it out! </Tip>Here are the most common ways you'll see callbacks in the wild.
The most common use of callbacks in browser JavaScript:
// DOM events
const button = document.getElementById('myButton')
button.addEventListener('click', function handleClick(event) {
console.log('Button clicked!')
console.log('Event type:', event.type) // "click"
console.log('Target:', event.target) // the button element
})
// The callback receives an Event object with details about what happened
You can also use named functions for reusability:
function handleClick(event) {
console.log('Clicked:', event.target.id)
}
function handleMouseOver(event) {
event.target.style.backgroundColor = 'yellow'
}
button.addEventListener('click', handleClick)
button.addEventListener('mouseover', handleMouseOver)
// Later, you can remove them:
button.removeEventListener('click', handleClick)
setTimeout and setInterval both accept callbacks:
// setTimeout - runs once after delay
const timeoutId = setTimeout(function() {
console.log('This runs once after 2 seconds')
}, 2000)
// Cancel it before it runs
clearTimeout(timeoutId)
// setInterval - runs repeatedly
let count = 0
const intervalId = setInterval(function() {
count++
console.log(`Count: ${count}`)
if (count >= 5) {
clearInterval(intervalId) // Stop after 5 times
console.log('Done!')
}
}, 1000)
Passing arguments to timer callbacks:
// Method 1: Closure (most common)
const name = 'Alice'
setTimeout(function() {
console.log(`Hello, ${name}!`)
}, 1000)
// Method 2: setTimeout's extra arguments
setTimeout(function(greeting, name) {
console.log(`${greeting}, ${name}!`)
}, 1000, 'Hello', 'Bob') // Extra args passed to callback
// Method 3: Arrow function with closure
const user = { name: 'Charlie' }
setTimeout(() => console.log(`Hi, ${user.name}!`), 1000)
These are synchronous callbacks, but they're everywhere:
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Phone', price: 699, inStock: false },
{ name: 'Tablet', price: 499, inStock: true }
]
// forEach - do something with each item
products.forEach(product => {
console.log(`${product.name}: $${product.price}`)
})
// map - transform each item into something new
const productNames = products.map(product => product.name)
// ['Laptop', 'Phone', 'Tablet']
// filter - keep only items that pass a test
const available = products.filter(product => product.inStock)
// [{ name: 'Laptop', ... }, { name: 'Tablet', ... }]
// find - get the first item that passes a test
const phone = products.find(product => product.name === 'Phone')
// { name: 'Phone', price: 699, inStock: false }
// reduce - combine all items into a single value
const totalValue = products.reduce((sum, product) => sum + product.price, 0)
// 2197
You can create your own functions that accept callbacks:
// A function that does something and then calls you back
function fetchUserData(userId, callback) {
// Simulate async operation
setTimeout(function() {
const user = { id: userId, name: 'Alice', email: '[email protected]' }
callback(user)
}, 1000)
}
// Using the function
fetchUserData(123, function(user) {
console.log('Got user:', user.name)
})
console.log('Fetching user...')
// Output:
// Fetching user...
// Got user: Alice (1 second later)
When Node.js came along, developers needed a standard way to handle errors in async callbacks. They landed on error-first callbacks (also called "Node-style callbacks" or "errbacks").
// Error-first callback signature
function callback(error, result) {
// error: null/undefined if success, Error object if failure
// result: the data if success, usually undefined if failure
}
The first parameter is always reserved for an error. If the operation succeeds, error is null or undefined. If it fails, error contains an Error object.
const fs = require('fs')
fs.readFile('config.json', 'utf8', function(error, data) {
// ALWAYS check for error first!
if (error) {
console.error('Failed to read file:', error.message)
return // Important: stop execution!
}
// If we get here, error is null/undefined
console.log('File contents:', data)
const config = JSON.parse(data)
console.log('Config loaded:', config)
})
function divideAsync(a, b, callback) {
// Simulate async operation
setTimeout(function() {
// Check for errors
if (typeof a !== 'number' || typeof b !== 'number') {
callback(new Error('Both arguments must be numbers'))
return
}
if (b === 0) {
callback(new Error('Cannot divide by zero'))
return
}
// Success! Error is null, result is the value
const result = a / b
callback(null, result)
}, 100)
}
// Using it
divideAsync(10, 2, function(error, result) {
if (error) {
console.error('Division failed:', error.message)
return
}
console.log('Result:', result) // Result: 5
})
divideAsync(10, 0, function(error, result) {
if (error) {
console.error('Division failed:', error.message) // "Cannot divide by zero"
return
}
console.log('Result:', result)
})
// ❌ WRONG - code continues after error callback!
function processData(data, callback) {
if (!data) {
callback(new Error('No data provided'))
// Oops! Execution continues...
}
// This runs even when there's an error!
const processed = transform(data) // Crash! data is undefined
callback(null, processed)
}
// ✓ CORRECT - return after error callback
function processData(data, callback) {
if (!data) {
return callback(new Error('No data provided'))
// Or: callback(new Error(...)); return;
}
// This only runs if data exists
const processed = transform(data)
callback(null, processed)
}
When you have multiple async operations that depend on each other, callbacks nest inside callbacks. This creates the infamous "callback hell" or "pyramid of doom."
Imagine a user authentication flow:
With callbacks, this becomes:
getUser(userId, function(error, user) {
if (error) {
handleError(error)
return
}
verifyPassword(user, password, function(error, isValid) {
if (error) {
handleError(error)
return
}
if (!isValid) {
handleError(new Error('Invalid password'))
return
}
getProfile(user.id, function(error, profile) {
if (error) {
handleError(error)
return
}
getSettings(user.id, function(error, settings) {
if (error) {
handleError(error)
return
}
renderDashboard(user, profile, settings, function(error) {
if (error) {
handleError(error)
return
}
console.log('Dashboard rendered!')
})
})
})
})
})
┌─────────────────────────────────────────────────────────────────────────┐
│ CALLBACK HELL │
│ (The Pyramid of Doom) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ getUser(id, function(err, user) { │
│ verifyPassword(user, pw, function(err, valid) { │
│ getProfile(user.id, function(err, profile) { │
│ getSettings(user.id, function(err, settings) { │
│ renderDashboard(user, profile, settings, function(err) { │
│ // Finally! But look at this indentation... │
│ }) │
│ }) │
│ }) │
│ }) │
│ }) │
│ │
│ Problems: │
│ • Hard to read (horizontal scrolling) │
│ • Hard to debug (which callback failed?) │
│ • Hard to maintain (adding a step means more nesting) │
│ • Error handling repeated at every level │
│ • Variables from outer callbacks hard to track │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Here's how to escape the pyramid of doom.
Extract anonymous callbacks into named functions:
// Before: Anonymous callback hell
getData(function(err, data) {
processData(data, function(err, processed) {
saveData(processed, function(err) {
console.log('Done!')
})
})
})
// After: Named functions
function handleData(err, data) {
if (err) return handleError(err)
processData(data, handleProcessed)
}
function handleProcessed(err, processed) {
if (err) return handleError(err)
saveData(processed, handleSaved)
}
function handleSaved(err) {
if (err) return handleError(err)
console.log('Done!')
}
function handleError(err) {
console.error('Error:', err.message)
}
// Start the chain
getData(handleData)
Benefits:
Keep the happy path at the lowest indentation level:
// Instead of nested if/else
function processUser(user, callback) {
validateUser(user, function(err, isValid) {
if (err) {
callback(err)
} else {
if (isValid) {
saveUser(user, function(err, savedUser) {
if (err) {
callback(err)
} else {
callback(null, savedUser)
}
})
} else {
callback(new Error('Invalid user'))
}
}
})
}
// Use early returns
function processUser(user, callback) {
validateUser(user, function(err, isValid) {
if (err) return callback(err)
if (!isValid) return callback(new Error('Invalid user'))
saveUser(user, function(err, savedUser) {
if (err) return callback(err)
callback(null, savedUser)
})
})
}
Split your code into smaller, focused modules:
// auth.js
function authenticateUser(credentials, callback) {
getUser(credentials.email, function(err, user) {
if (err) return callback(err)
verifyPassword(user, credentials.password, function(err, isValid) {
if (err) return callback(err)
if (!isValid) return callback(new Error('Invalid password'))
callback(null, user)
})
})
}
// profile.js
function loadUserProfile(userId, callback) {
getProfile(userId, function(err, profile) {
if (err) return callback(err)
getSettings(userId, function(err, settings) {
if (err) return callback(err)
callback(null, { profile, settings })
})
})
}
// main.js
authenticateUser(credentials, function(err, user) {
if (err) return handleError(err)
loadUserProfile(user.id, function(err, data) {
if (err) return handleError(err)
renderDashboard(user, data.profile, data.settings)
})
})
Before Promises, libraries like async.js helped manage callback flow:
// Using async.js waterfall (each step passes result to next)
async.waterfall([
function(callback) {
getUser(userId, callback)
},
function(user, callback) {
verifyPassword(user, password, function(err, isValid) {
callback(err, user, isValid)
})
},
function(user, isValid, callback) {
if (!isValid) return callback(new Error('Invalid password'))
getProfile(user.id, function(err, profile) {
callback(err, user, profile)
})
},
function(user, profile, callback) {
getSettings(user.id, function(err, settings) {
callback(err, user, profile, settings)
})
}
], function(err, user, profile, settings) {
if (err) return handleError(err)
renderDashboard(user, profile, settings)
})
Promises were invented specifically to solve callback hell:
// The same flow with Promises
getUser(userId)
.then(user => verifyPassword(user, password))
.then(({ user, isValid }) => {
if (!isValid) throw new Error('Invalid password')
return getProfile(user.id).then(profile => ({ user, profile }))
})
.then(({ user, profile }) => {
return getSettings(user.id).then(settings => ({ user, profile, settings }))
})
.then(({ user, profile, settings }) => {
renderDashboard(user, profile, settings)
})
.catch(handleError)
Or with async/await:
// The same flow with async/await
async function initDashboard(userId, password) {
try {
const user = await getUser(userId)
const isValid = await verifyPassword(user, password)
if (!isValid) throw new Error('Invalid password')
const profile = await getProfile(user.id)
const settings = await getSettings(user.id)
renderDashboard(user, profile, settings)
} catch (error) {
handleError(error)
}
}
A callback should typically be called exactly once, either with an error or with a result:
// ❌ WRONG - callback called multiple times!
function fetchData(url, callback) {
fetch(url)
.then(response => {
callback(null, response) // Called on success
})
.catch(error => {
callback(error) // Called on error
})
.finally(() => {
callback(null, 'done') // Called ALWAYS, even after success or error!
})
}
// ✓ CORRECT - callback called exactly once
function fetchData(url, callback) {
fetch(url)
.then(response => callback(null, response))
.catch(error => callback(error))
}
A function should be consistently sync or async, never both. This inconsistency is nicknamed "releasing Zalgo," a reference to an internet meme about unleashing chaos. And chaos is exactly what you get when code behaves unpredictably:
// ❌ WRONG - sometimes sync, sometimes async (Zalgo!)
function getData(cache, callback) {
if (cache.has('data')) {
callback(null, cache.get('data')) // Sync!
return
}
fetchFromServer(function(err, data) {
callback(err, data) // Async!
})
}
// This causes unpredictable behavior:
let value = 'initial'
getData(cache, function(err, data) {
value = data
})
console.log(value) // "initial" or the data? Depends on cache!
// ✓ CORRECT - always async
function getData(cache, callback) {
if (cache.has('data')) {
// Use setTimeout to make it async (works in browsers and Node.js)
setTimeout(function() {
callback(null, cache.get('data'))
}, 0)
return
}
fetchFromServer(function(err, data) {
callback(err, data)
})
}
this ContextRegular functions lose their this binding when used as callbacks:
// ❌ WRONG - this is undefined/global
const user = {
name: 'Alice',
greetLater: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}!`) // this.name is undefined!
}, 1000)
}
}
user.greetLater() // "Hello, undefined!"
// ✓ CORRECT - Use arrow function (inherits this)
const user = {
name: 'Alice',
greetLater: function() {
setTimeout(() => {
console.log(`Hello, ${this.name}!`) // Arrow function keeps this
}, 1000)
}
}
user.greetLater() // "Hello, Alice!"
// ✓ CORRECT - Use bind
const user = {
name: 'Alice',
greetLater: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}!`)
}.bind(this), 1000) // Explicitly bind this
}
}
user.greetLater() // "Hello, Alice!"
// ✓ CORRECT - Save reference to this
const user = {
name: 'Alice',
greetLater: function() {
const self = this // Save reference
setTimeout(function() {
console.log(`Hello, ${self.name}!`)
}, 1000)
}
}
user.greetLater() // "Hello, Alice!"
Always handle errors in async callbacks. Unhandled errors can crash your application:
// ❌ WRONG - error ignored
fs.readFile('config.json', function(err, data) {
const config = JSON.parse(data) // Crashes if err exists!
startApp(config)
})
// ✓ CORRECT - error handled
fs.readFile('config.json', function(err, data) {
if (err) {
console.error('Could not read config:', err.message)
process.exit(1)
return
}
try {
const config = JSON.parse(data)
startApp(config)
} catch (parseError) {
console.error('Invalid JSON in config:', parseError.message)
process.exit(1)
}
})
Understanding why JavaScript uses callbacks helps everything click into place.
JavaScript was created by Brendan Eich at Netscape in just 10 days. Its primary purpose was to make web pages interactive, responding to user clicks, form submissions, and other events.
JavaScript was designed to be single-threaded: one thing at a time. Why?
But single-threaded means a problem: you can't block waiting for things.
If JavaScript waited for a network request to complete, the entire page would freeze. Users couldn't click, scroll, or do anything. That's unacceptable for a UI language.
Callbacks solved this problem neatly:
// This pattern was there from day one
element.onclick = function() {
alert('Clicked!')
}
// The page doesn't freeze waiting for a click
// JavaScript registers the callback and moves on
// When clicked, the callback runs
| Year | Development |
|---|---|
| 1995 | JavaScript created with event callbacks |
| 1999 | XMLHttpRequest (AJAX) — async HTTP with callbacks |
| 2009 | Node.js — callbacks for server-side I/O |
| 2012 | Callback hell becomes a recognized problem |
| 2015 | ES6 Promises — official solution to callback hell |
| 2017 | ES8 async/await — syntactic sugar for Promises |
Even with Promises and async/await, callbacks are everywhere:
.then(callback))Callbacks aren't obsolete. They're the foundation that everything else builds upon.
A callback is a function passed to another function to be executed later — nothing magical
Callbacks can be synchronous or asynchronous — array methods are sync, timers and events are async
Higher-order functions and callbacks are two sides of the same coin — one accepts, one is passed
Async callbacks go through the event loop — they never run until all sync code finishes
Error-first callbacks: callback(error, result) — always check error first, return after handling
You can't use try/catch for async callbacks — the catch is gone by the time the callback runs
Callback hell is real — deeply nested callbacks become unreadable and unmaintainable
Escape callback hell with: named functions, modularization, early returns, or Promises
Promises were invented to solve callback problems — but they still use callbacks under the hood
Callbacks are the foundation — events, Promises, async/await all build on callbacks
</Info>**Synchronous callbacks** execute immediately, during the function call. They block until complete. Examples: `map`, `filter`, `forEach`.
```javascript
[1, 2, 3].forEach(n => console.log(n)) // Runs immediately, blocks
console.log('Done') // Runs after forEach completes
```
**Asynchronous callbacks** execute later, via the event loop. They don't block. Examples: `setTimeout`, `addEventListener`, `fs.readFile`.
```javascript
setTimeout(() => console.log('Timer'), 0) // Registers, doesn't block
console.log('Done') // Runs BEFORE the timer callback
```
The error-first convention exists because:
1. **Consistency** — Every async callback has the same signature: `(error, result)`
2. **Can't be ignored** — The error is the first thing you must deal with
3. **Forces handling** — You naturally check for errors before using results
4. **No exceptions** — Async errors can't be caught with try/catch, so they must be passed
```javascript
fs.readFile('file.txt', (error, data) => {
if (error) {
// Handle error FIRST
console.error(error)
return
}
// Safe to use data
console.log(data)
})
```
setTimeout(() => console.log('B'), 0)
console.log('C')
setTimeout(() => console.log('D'), 0)
console.log('E')
```
**Answer:** `A`, `C`, `E`, `B`, `D`
**Explanation:**
1. `console.log('A')` — sync, runs immediately → "A"
2. `setTimeout(..., 0)` — registers callback B, continues
3. `console.log('C')` — sync, runs immediately → "C"
4. `setTimeout(..., 0)` — registers callback D, continues
5. `console.log('E')` — sync, runs immediately → "E"
6. Call stack empty → event loop runs callback B → "B"
7. Event loop runs callback D → "D"
Even with 0ms delay, setTimeout callbacks run after all sync code.
**1. Arrow functions** (recommended — they inherit `this` from enclosing scope):
```javascript
const obj = {
name: 'Alice',
greet() {
setTimeout(() => {
console.log(this.name) // "Alice"
}, 100)
}
}
```
**2. Using `bind()`**:
```javascript
setTimeout(function() {
console.log(this.name)
}.bind(this), 100)
```
**3. Saving a reference**:
```javascript
const self = this
setTimeout(function() {
console.log(self.name)
}, 100)
```
The `try/catch` block executes **synchronously**. By the time an async callback runs, the try/catch is long gone. It's on a different "turn" of the event loop.
```javascript
try {
setTimeout(() => {
throw new Error('Async error!') // This escapes!
}, 100)
} catch (e) {
// This NEVER catches the error
console.log('Caught:', e)
}
// The error crashes the program because:
// 1. try/catch runs immediately
// 2. setTimeout registers callback and returns
// 3. try/catch completes (nothing thrown yet!)
// 4. 100ms later, callback runs and throws
// 5. No try/catch exists at that point
```
This is why we use error-first callbacks or Promise `.catch()` for async error handling.
**1. Named functions** — Extract callbacks into named functions:
```javascript
function handleUser(err, user) {
if (err) return handleError(err)
getProfile(user.id, handleProfile)
}
getUser(userId, handleUser)
```
**2. Modularization** — Split into separate modules/functions:
```javascript
// auth.js exports authenticateUser()
// profile.js exports loadProfile()
// main.js composes them
```
**3. Promises/async-await** — Use modern async patterns:
```javascript
const user = await getUser(userId)
const profile = await getProfile(user.id)
```
Other approaches: control flow libraries (async.js), early returns, keeping nesting shallow.