What is error-first callback pattern?
Error-first callback pattern - a Node.js convention where async functions always pass an error as the first argument to their callback, followed by result data if no error occurred.
Theory
TL;DR
- Think of it like a delivery driver: first they report if there was a problem (error), then hand over the package (data) only if everything went fine
- Convention:
callback(err, data)- error first, data second - On success:
errisnull,dataholds the result; on failure:erris anErrorobject anddataisundefined - Always check
if (err)before touchingdata, and alwaysreturnafter - skipping the return is the #1 bug - Use for
fs,mysql,pgand similar libs; prefer Promises for new code
Quick example
const fs = require('fs');
fs.readFile('config.json', 'utf8', (err, content) => {
if (err) {
console.error('Failed to read:', err.message); // ENOENT: no such file...
return; // Critical - stops execution here
}
console.log(content); // { port: 3000 } - only runs if no error
});Error arrives first. If the file is missing, err is set and content is undefined. The return keeps the rest of the callback from running with bad data.
Why error comes first
In synchronous code, you throw an exception and a try-catch somewhere up the stack catches it. That doesn't work for async code. The callback runs later, in a different turn of the event loop, so the original try-catch is long gone. Putting the error first forces you to handle it manually, every time. There is no way to accidentally skip it.
When to use
- Reading or writing files with
fs: use error-first callbacks - Database queries with
mysqlorpg: both follow this convention - Writing a custom async function: pass
(err, result)to match Node stdlib - Wrapping a legacy third-party lib: error-first keeps things consistent
- New code on Node 10+: prefer
fs.promisesand async/await instead
How Node.js handles it internally
libuv threads manage I/O operations like fs.readFile. When the op finishes, libuv passes a C-level error code (for example, UV_ENOENT) or zero back to V8. The event loop then calls your callback with either an Error built from that code, or null. The pattern itself is a convention - nothing in the runtime enforces it, which is exactly why forgetting return is such a common bug.
Common mistakes
Forgetting return after handling the error
// Wrong - falls through to data block
fs.readFile('bad.txt', (err, data) => {
if (err) console.error(err); // No return!
console.log(data); // Runs anyway - logs undefined
});
// Right
fs.readFile('bad.txt', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});Without return, execution continues with data === undefined. I've seen this cause silent writes of undefined to production databases - the code appears to work but corrupts records.
Swapped argument order in a custom function
// Wrong - breaks every caller
function getUser(id, cb) {
db.query(sql, [id], (err, row) => cb(row, err)); // swapped!
}
// Right
function getUser(id, cb) {
db.query(sql, [id], (err, row) => cb(err, row));
}Every Node.js lib expects (err, data). Swap the arguments and callers end up checking the wrong one.
Wrapping async code in try-catch
// Does nothing useful
try {
fs.readFile('file.txt', (err, data) => {
throw new Error('boom'); // Not caught by the outer try-catch
});
} catch (e) {
console.error(e); // Never runs
}The try-catch exits before the callback fires. Handle errors inside the callback.
Real-world usage
fs.readFile,fs.writeFile: Node.js core, this pattern since v0.1.90mysqljs/mysql:connection.query(sql, params, (err, rows) => {})- over 10M weekly downloadspg(Postgres):client.query(sql, (err, result) => {})- standard in most Node backends- Express: the
(err, req, res, next)error middleware signature extends this convention - Converting to Promise:
util.promisify(fs.readFile)('file.txt').then(...).catch(...)
Follow-up questions
Q: Why doesn't Node.js just throw exceptions for async errors?
A: Async callbacks run after the original call stack is gone. A thrown exception inside a callback has nowhere to be caught and crashes the process. Error-first keeps control inside the callback.
Q: How do you convert an error-first callback to a Promise?
A: Use util.promisify: const readFile = util.promisify(fs.readFile). Then call it with .then().catch() or inside async/await.
Q: What if you need to return multiple results?
A: Pass an object or array as the second argument: cb(null, { user, token }). One error, one result - pack multiple values together.
Q: In a chain of nested callbacks, how do you avoid writing if (err) return cb(err) in every step?
A: Extract each step into a named function, or use the async library's waterfall. For new code, async/await solves this with a single try-catch over the whole chain.
Examples
Basic: file read with error propagation
const fs = require('fs');
function readConfig(path, cb) {
fs.readFile(path, 'utf8', (err, content) => {
if (err) return cb(err); // Propagate error up
cb(null, JSON.parse(content)); // Pass parsed data on success
});
}
readConfig('config.json', (err, config) => {
if (err) {
console.error('Config load failed:', err.message); // ENOENT...
return;
}
console.log('Port:', config.port); // Port: 3000
});cb(err) propagates errors without hiding them. cb(null, result) signals success. The caller handles both paths.
Intermediate: Postgres query in Express
const express = require('express');
const pg = require('pg');
const app = express();
app.get('/user/:id', (req, res) => {
const client = new pg.Client();
client.connect((err) => {
if (err) return res.status(500).json({ error: err.message });
client.query(
'SELECT * FROM users WHERE id = $1',
[req.params.id],
(err, result) => {
client.end();
if (err) return res.status(500).json({ error: err.message });
res.json(result.rows[0]); // { id: 1, name: 'Alice' }
}
);
});
});Each async step checks err before going further. return on each error path prevents sending two responses. This nesting pattern is what eventually pushed the community toward Promises.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.