Node.js 回调函数
error-first 约定、回调地狱、以及它为什么会被 Promise 取代。
Key PointsNode 的约定是 `callback(err, result)`:错误放第一个参数,成功时它是 `null`。
error-first 约定
JavaScript
const fs = require('node:fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('读失败:', err.code); // ENOENT
return;
}
console.log('内容长度:', data.length);
});Output
读失败: ENOENTWhy first? A callback has one slot and must express both "failed" and "succeeded with a falsy value". Put the error second and null results become indistinguishable from failure.
自己写一个
JavaScript
function fetchUser(id, callback) {
setTimeout(() => {
if (!Number.isInteger(id)) {
return callback(new TypeError('id 必须是整数'));
}
callback(null, { id, name: 'user-' + id });
}, 100);
}
fetchUser(7, (err, user) => {
if (err) return console.error(err.message);
console.log(user);
});Output
{ id: 7, name: 'user-7' }回调地狱
JavaScript
// 三步依赖:读配置 → 连库 → 查数据
fs.readFile('config.json', 'utf8', (err, cfg) => {
if (err) return done(err);
db.connect(JSON.parse(cfg).url, (err, conn) => {
if (err) return done(err);
conn.query('SELECT 1', (err, rows) => {
if (err) return done(err);
conn.close((err) => {
if (err) return done(err);
done(null, rows); // 四层缩进,错误要每层判断
});
});
});
});The problem is not nesting per se, but that every step repeats error checks, try/catch does not work, parallel composition is impossible and cancellation is impossible. Promise exists to fix those four.
转成 Promise
JavaScript
const { promisify } = require('node:util');
const readFile = promisify(fs.readFile);
// 或者手写
function fetchUserP(id) {
return new Promise((resolve, reject) => {
fetchUser(id, (err, user) => (err ? reject(err) : resolve(user)));
});
}
// 现在可以这样写
const user = await fetchUserP(7);Node's
fs, dns and crypto already ship a promises namespace — require('node:fs/promises') is cleaner than promisify.error-first 约定
JavaScript
const fs = require('node:fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('读失败:', err.code); // ENOENT
return;
}
console.log('内容长度:', data.length);
});Output
读失败: ENOENT为什么错误放第一个?因为回调只有一个位置,必须能同时表达「失败」和「成功但结果是假值」。如果错误放第二个,null 结果和失败就无法区分。
自己写一个
JavaScript
function fetchUser(id, callback) {
setTimeout(() => {
if (!Number.isInteger(id)) {
return callback(new TypeError('id 必须是整数'));
}
callback(null, { id, name: 'user-' + id });
}, 100);
}
fetchUser(7, (err, user) => {
if (err) return console.error(err.message);
console.log(user);
});Output
{ id: 7, name: 'user-7' }回调地狱
JavaScript
// 三步依赖:读配置 → 连库 → 查数据
fs.readFile('config.json', 'utf8', (err, cfg) => {
if (err) return done(err);
db.connect(JSON.parse(cfg).url, (err, conn) => {
if (err) return done(err);
conn.query('SELECT 1', (err, rows) => {
if (err) return done(err);
conn.close((err) => {
if (err) return done(err);
done(null, rows); // 四层缩进,错误要每层判断
});
});
});
});问题不在嵌套本身,而在于:每一步都要重复写错误判断、**无法用 try/catch、无法并行组合、无法中途取消**。Promise 就是为解这四个问题设计的。
转成 Promise
JavaScript
const { promisify } = require('node:util');
const readFile = promisify(fs.readFile);
// 或者手写
function fetchUserP(id) {
return new Promise((resolve, reject) => {
fetchUser(id, (err, user) => (err ? reject(err) : resolve(user)));
});
}
// 现在可以这样写
const user = await fetchUserP(7);Node 的
fs、dns、crypto 等模块已经内置了 promises 命名空间,直接用 require('node:fs/promises') 比 promisify 更干净。