Node.js 回调函数

error-first 约定、回调地狱、以及它为什么会被 Promise 取代。

Intermediate
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
读失败: ENOENT

Why 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.

Friends & Partners