JavaScript 错误处理

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Node.js 22 LTS

程序运行出错不可怕,可怕的是错误被静默吞掉,最后表现为「数据不对但不知道哪里错了」。这一篇讲如何抛出、捕获和传递错误。

Error 对象

const err = new Error("文件不存在");
err.name;       // "Error"
err.message;    // "文件不存在"
err.stack;      // 调用栈字符串,调试用

Error 只是普通对象,创建它不会中断程序;要中断必须用 throw。

常见内置错误类型:

类型 触发场景
TypeError 对错误类型做操作,如 null.name、nums.map 不是函数
ReferenceError 访问未声明的变量
SyntaxError 代码语法错误(JSON.parse 遇到非法字符串也抛它)
RangeError 数值越界,如 new Array(-1)

throw:主动中断

function divide(a, b) {
  if (b === 0) {
    throw new Error("除数不能为 0");
  }
  return a / b;
}

抛出后函数立即结束,后面的代码不执行,控制权交给最近的 catch;没有 catch 则程序终止(Node 里进程退出,浏览器里控制台报错)。

只 throw Error 实例:

throw new Error("出错了");    // 正确
throw "出错了";               // 不推荐:没有 stack,catch 里拿不到结构化信息

try / catch / finally

try {
  const data = JSON.parse(raw);
  console.log(data);
} catch (err) {
  console.error("解析失败:", err.message);
} finally {
  console.log("这一步总会执行");
}

执行顺序:

  1. try 里出错 → 跳到 catch,出错点之后的代码不执行。
  2. 无论是否出错、catch 里是否又有 return 或 throw,finally 总会执行。
  3. finally 常用于释放资源:关闭文件、清除定时器、恢复加载状态。
let busy = true;
try {
  await loadData();
} finally {
  busy = false;   // 出错也一定恢复状态
}

catch 里的错误类型判断

try {
  JSON.parse(raw);
} catch (err) {
  if (err instanceof SyntaxError) {
    console.log("数据格式不合法");
  } else {
    throw err;   // 不认识的错误继续向上抛
  }
}

「处理你能处理的,其余继续抛」是关键习惯。无差别吞掉所有错误会让问题消失在日志里。

ES2022 起可以省略 catch 参数:

try {
  doSomething();
} catch {
  console.log("失败了");   // 不关心错误细节
}

自定义错误类型

业务代码里区分错误类型比读 message 字符串可靠:

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function requireName(user) {
  if (!user.name) {
    throw new ValidationError("缺少用户名", "name");
  }
}

try {
  requireName({});
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`${err.field}: ${err.message}`);   // "name: 缺少用户名"
  }
}

继承时记得设置 this.name,否则 err.name 会是 "Error"。

错误上下文:cause

try {
  readConfig();
} catch (err) {
  throw new Error("启动失败", { cause: err });
}

上层通过 err.cause 拿到原始错误,避免为了保留原因去拼接 message 字符串。Node 18+ 与所有现代浏览器支持 cause。

异步错误不能用 try / catch 直接捕获

这是最容易踩的坑:

// 错误写法:catch 不到
try {
  setTimeout(() => {
    throw new Error("异步出错");
  }, 100);
} catch (err) {
  console.log("捕获到了");   // 永远不会执行
}

回调执行时 try 块早已结束,错误发生在另一个调用栈里。正确做法取决于异步风格:

Promise 用 .catch():

fetchUsers()
  .then((users) => console.log(users))
  .catch((err) => console.error("请求失败:", err.message));

async / await 可以用 try / catch(推荐):

async function load() {
  try {
    const users = await fetchUsers();
    return users;
  } catch (err) {
    console.error("请求失败:", err.message);
    return [];
  }
}

Promise 链的末尾必须挂 .catch(),否则会产生未处理的 rejection(Node 里会打印警告或在未来版本中直接终止进程)。

输入校验优先于异常

异常应该表示「异常情况」,不要用 try / catch 代替正常的判断:

// 不推荐:把正常分支交给异常
try {
  const value = obj.field;
} catch {
  const value = "默认";
}

// 推荐:直接判断
const value = obj?.field ?? "默认";

常见错误与注意事项

  1. 空的 catch {}:吞掉错误,问题被隐藏。至少要记录日志,或写明为什么可以忽略。
  2. throw 字符串或普通对象:拿不到 stack,instanceof 也无法判断类型。
  3. 用 try / catch 包 setTimeout 回调:捕获不到异步错误,改用 Promise + await。
  4. finally 里 return:会覆盖 try / catch 的返回值,通常不是你想要的。
  5. Promise 链忘记 .catch():未处理的 rejection 难以定位。
  6. 自定义错误不设置 name:日志里清一色 "Error",排查困难。

小结

  • throw 只抛 Error 实例;new Error(msg, { cause }) 保留原始原因。
  • try / catch / finally 里 finally 一定执行,适合清理。
  • 只处理认识的错误,其余用 throw err 继续上抛。
  • 异步错误靠 .catch() 或 await + try / catch,直接 try 包回调无效。
  • 正常分支用判断,不要用异常代替。

下一步