JavaScript 错误处理:throw、try 和 catch

在 JavaScript 中,错误处理是构建健壮应用程序的关键。通过 try...catch...finally 结构和 throw 语句,我们可以优雅地捕获、处理和抛出错误,防止程序崩溃并提供更好的用户体验。


一、错误类型 (Error Types)

JavaScript 内置了多种错误类型,了解它们有助于精准捕获和处理。

错误类型 描述 常见触发场景
Error 通用错误基类 手动抛出或未知错误
SyntaxError 语法错误 eval() 中的非法语法,JSON 解析错误
ReferenceError 引用错误 访问未声明的变量
TypeError 类型错误 对非对象调用方法,null/undefined 调用
RangeError 范围错误 数组长度非法,递归过深
URIError URI 错误 decodeURI() 参数非法
EvalError Eval 错误 eval() 使用不当 (现代 JS 中很少见)
// 示例
let a;
console.log(a); // undefined (不是错误)
console.log(b); // ReferenceError: b is not defined

"hello".length; // 5 (正常)
(5).length;     // TypeError: Cannot read property 'length' of number

JSON.parse("{a:1}"); // SyntaxError: Unexpected token a

二、try…catch…finally 结构

这是 JavaScript 处理同步错误的核心机制。

1. 基本语法

try {
  // 可能抛出错误的代码
  riskyOperation();
} catch (error) {
  // 捕获并处理错误
  console.error("发生错误:", error.message);
} finally {
  // 无论是否出错都会执行的代码 (清理资源)
  console.log("清理工作完成");
}

2. 执行流程

  1. try: 执行代码。
    • 若无错误:跳过 catch,执行 finally (如果有),然后继续后续代码。
    • 若有错误:立即停止 try 中剩余代码,跳转到 catch
  2. catch: 接收错误对象。
    • 执行错误处理逻辑。
    • 如果 catch 中再次抛出错误,finally 仍会执行,然后错误继续向上传播。
  3. finally: 无论发生什么都会执行。
    • 常用于关闭文件、清除定时器、隐藏加载动画等。

3. 代码示例

function divide(a, b) {
  try {
    if (b === 0) {
      throw new Error("除数不能为零");
    }
    return a / b;
  } catch (err) {
    console.error(`计算失败: ${err.message}`);
    return null; // 返回默认值
  } finally {
    console.log("除法操作结束");
  }
}

console.log(divide(10, 2)); // 5, "除法操作结束"
console.log(divide(10, 0)); // null, "计算失败: 除数不能为零", "除法操作结束"

三、throw 语句

throw 用于手动抛出错误。抛出的值可以是任何类型,但通常抛出 Error 对象或其子类。

1. 抛出标准错误对象

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError("年龄必须是数字");
  }
  if (age < 0 || age > 150) {
    throw new RangeError("年龄必须在 0 到 150 之间");
  }
  return true;
}

try {
  validateAge("20"); // 抛出 TypeError
} catch (e) {
  console.log(e.name);   // "TypeError"
  console.log(e.message); // "年龄必须是数字"
  console.log(e.stack);   // 堆栈跟踪信息
}

2. 抛出自定义错误 (ES2022+)

现代 JavaScript 允许直接继承 Error 创建自定义错误类,使错误处理更语义化。

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
    // 保持堆栈跟踪正确 (在 V8 引擎中)
    Error.captureStackTrace(this, ValidationError);
  }
}

function registerUser(user) {
  if (!user.email) {
    throw new ValidationError("邮箱不能为空", "email");
  }
  if (!user.password || user.password.length < 6) {
    throw new ValidationError("密码长度至少6位", "password");
  }
}

try {
  registerUser({ name: "Alice" });
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`字段 ${err.field} 验证失败:${err.message}`);
  }
}

3. 抛出非 Error 对象 (不推荐)

虽然可以抛出字符串或数字,但这会导致调试困难,且丢失堆栈信息。

// 不推荐
throw "出错了"; 
throw 404;

// 推荐
throw new Error("出错了");
throw { code: 404, message: "Not Found" }; // 如果必须用对象,也要小心处理

四、错误处理最佳实践

1. 捕获特定错误

避免使用空的 catch 或捕获所有错误而不区分,这可能会掩盖真正的 bug。

// 不推荐:捕获所有错误并忽略
try {
  risky();
} catch (e) {
  // 什么都不做,错误被吞掉了
}

// 推荐:区分处理
try {
  risky();
} catch (e) {
  if (e instanceof NetworkError) {
    showRetryButton();
  } else if (e instanceof ValidationError) {
    showFormError(e.field);
  } else {
    // 未知错误,记录日志并上报
    console.error("未知错误", e);
    reportToSentry(e);
  }
}

2. 不要吞掉错误

catch 块中,如果无法处理,应该重新抛出 (throw e) 或记录日志。

try {
  doSomething();
} catch (e) {
  console.error("处理失败", e);
  throw e; // 重新抛出,让上层处理
}

3. 使用 finally 清理资源

确保资源(如定时器、文件流、锁)被释放。

let timer;
try {
  timer = setInterval(() => {
    // ...
  }, 1000);
  // 可能出错的代码
} catch (e) {
  console.error(e);
} finally {
  if (timer) clearInterval(timer); // 确保定时器被清除
}

4. 异步错误处理

try...catch 不能直接捕获异步回调(如 setTimeout)中的错误,但可以捕获 async/await 中的错误。

场景 A: Promise 链
fetchData()
  .then(data => processData(data))
  .catch(err => {
    console.error("Promise 错误:", err);
  });
场景 B: Async/Await (推荐)
async function loadData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (err) {
    console.error("加载失败:", err);
    throw err; // 重新抛出
  } finally {
    setLoading(false);
  }
}
场景 C: 全局未捕获错误

对于未被 catch 捕获的 Promise 拒绝,需要监听全局事件:

window.addEventListener('unhandledrejection', event => {
  event.preventDefault(); // 阻止默认控制台报错
  console.error('未处理的 Promise 错误:', event.reason);
});

五、常见陷阱与技巧

1. 错误对象属性

属性 含义
name 错误名称 (如 “TypeError”)
message 错误描述信息
stack 堆栈跟踪字符串 (调试神器)
try {
  undefined.func();
} catch (e) {
  console.log(e.name);    // "TypeError"
  console.log(e.message); // "Cannot read properties of undefined"
  console.log(e.stack);   // 包含文件行号
}

2. 重新抛出错误

catch 中处理部分逻辑后,如果需要上层知道错误,必须 throw

try {
  // ...
} catch (e) {
  logError(e);
  // 忘记 throw e; -> 错误被吞掉,上层以为成功了
  throw e; 
}

3. 错误边界 (React 等框架)

在 UI 框架中,通常有专门的“错误边界”组件来捕获子组件渲染时的错误,防止整个应用崩溃。

4. 调试技巧

  • catch 中打断点,查看 error 对象。
  • 使用 console.trace() 在抛出前打印调用栈。
  • 使用 debugger 语句配合 try...catch 定位问题。

六、总结

关键字 作用 关键点
try 包裹可能出错的代码 必须与 catchfinally 配合使用
catch 捕获并处理错误 接收错误对象,可区分类型处理
finally 无论成败都执行的清理代码 常用于资源释放
throw 手动抛出错误 推荐抛出 Error 对象或子类

核心原则

  1. 不要吞掉错误:要么处理,要么抛出。
  2. 精准捕获:尽量捕获特定类型的错误。
  3. 保持堆栈:抛出错误时保留原始堆栈信息。
  4. 异步处理:使用 async/await + try/catch 处理异步错误。

掌握这些机制,能让你的 JavaScript 代码更加健壮、易维护!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐