Introduction
The error-first callback pattern (also called 'Node-style callbacks') is a convention used throughout Node.js core APIs and the broader ecosystem: an async function accepts a callback whose first parameter is always reserved for an error object (or null if no error occurred), followed by any result data. This consistent shape allows developers to predictably check for failures before touching the result, and lets utility functions and libraries interoperate without guessing argument order.
Cricket analogy: Every over's report to the scorer follows one convention: state any irregularity first — a no-ball, a wide — before reporting the runs scored, so the scoreboard operator always checks for an issue before recording the total, no matter which umpire is signaling.
Syntax
function doAsyncTask(input, callback) {
// callback signature: (err, result)
process.nextTick(() => {
if (typeof input !== 'string') {
return callback(new Error('input must be a string'));
}
callback(null, input.toUpperCase());
});
}
doAsyncTask('hello', (err, result) => {
if (err) {
console.error('Failed:', err.message);
return;
}
console.log('Result:', result);
});Explanation
By convention, when an operation fails, callback(err) is invoked with an Error instance (and typically no further arguments, or undefined for the result). When it succeeds, callback(null, result) is invoked with null explicitly signaling 'no error'. Consumers are expected to check if (err) first and return/handle it before using the result, preventing accidental use of undefined or invalid data. Node's built-in modules like fs, dns, and crypto all follow this pattern, which is why libraries such as the async module and util.promisify are built around it.
Cricket analogy: When a review fails, the third umpire reports the specific problem first — 'insufficient evidence' — with no result attached; when it succeeds, they explicitly confirm 'no issue' before revealing the decision, and the on-field umpire always checks for a flagged problem before acting on the outcome, exactly the convention fs, dns, and crypto follow in Node, letting tools like util.promisify convert any such report into a clean pass/fail.
Example
const fs = require('fs');
const util = require('util');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Could not read config:', err.message);
return;
}
console.log('Config loaded:', JSON.parse(data));
});
// Converting an error-first callback API into a Promise-based one
const readFilePromise = util.promisify(fs.readFile);
async function loadConfig() {
try {
const data = await readFilePromise('config.json', 'utf8');
console.log('Config loaded via promisify:', JSON.parse(data));
} catch (err) {
console.error('Could not read config:', err.message);
}
}Output
If config.json exists and is valid JSON, both approaches log the parsed config object. If the file is missing, fs.readFile invokes the callback with an ENOENT Error as err, so the code logs 'Could not read config: ENOENT: no such file or directory...' and never attempts to parse undefined data. util.promisify() automatically converts any error-first callback function into one that returns a Promise, rejecting with err when callback(err, ...) is called and resolving with the result argument(s) otherwise — this is exactly why the convention's consistency matters.
Cricket analogy: If the pitch report file is missing, the analytics system logs 'Could not read pitch report: ENOENT' and never attempts to parse a nonexistent report; util.promisify turns that same lookup into a promise that simply rejects with the missing-file error or resolves with the parsed conditions — same information, cleaner handling.
Key Takeaways
- The callback signature is always (err, result, ...): error first, then success data.
- callback(null, result) signals success; callback(err) (with err truthy) signals failure.
- Always check
if (err)before using the result to avoid using invalid/undefined data. - This convention is what makes util.promisify() able to auto-convert callback APIs to Promises.
- Most Node.js core modules (fs, dns, child_process, crypto) follow this pattern.
Practice what you learned
1. In the error-first callback pattern, what is passed as the first argument to the callback?
2. What does calling `callback(null, result)` signal?
3. Why does util.promisify() work well with functions following the error-first pattern?
4. What is a common bug when consuming error-first callbacks?
Was this page helpful?
You May Also Like
Callbacks in Node.js
How Node.js uses callback functions to handle asynchronous operations without blocking the main thread.
Promises in Node.js
Understanding the Promise object and its pending, fulfilled, and rejected states for cleaner async code.
Error Handling in Express
Master synchronous and asynchronous error handling in Express using 4-argument error middleware.
The fs Module in Node.js
Learn how to read, write, and manage files using Node's built-in fs module with sync, async, and promise-based APIs.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics