错误处理与调试
让程序在出错时不崩,在调试时一目了然
🎯 学习目标
学完本章,你将能够:
- 能够使用 try/catch/finally 捕获和处理同步代码中的错误
- 能够区分 JavaScript 内置错误类型(TypeError、SyntaxError、RangeError 等)
- 能够创建自定义错误类,携带额外的上下文信息
- 能够使用浏览器 DevTools 设置断点、单步执行、查看调用栈
- 能够运用防御性编程策略,编写健壮的错误处理代码
你有没有想过
为什么有些网站出错了还能正常用,有些网站一个接口挂了整个页面白屏?
差别就在错误处理。好的程序不是不报错,而是报错了还能优雅降级、给用户友好提示、记录日志方便排查。
这一章你将学会:用 try/catch 捕获错误、自定义错误类型、异步错误处理策略、以及浏览器和 Node.js 的调试技巧。
Part 1: try/catch
最基本的错误处理结构:把可能出错的代码包起来,出了错能接住
一个接口返回了格式错误的数据,如果不处理,整个页面直接白屏。但如果用 try/catch 包起来,最起码能给用户一个"数据加载失败"的友好提示。这是最基础也最有效的防线。finally 块不管出不出错都会执行,适合做清理工作。先把这个三板斧练熟,后面的高级技巧才有根基。
try/catch/finally
try 里出错了不会炸,catch 接住它,finally 始终执行
try {
let data = JSON.parse("invalid json"); // 这里会抛 SyntaxError
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // 具体错误信息
console.log(error.stack); // 完整调用栈(排查利器)
} finally {
console.log("始终执行,不管有没有错");
}
用比喻来说:try 是蹦极,catch 是安全网,finally 是着地后无论安不安全都要做的收尾。
嵌套与重新抛出
内层处理不了的错误,throw 给外层
try {
try {
throw new Error("内部错误");
} catch (e) {
console.log("内部捕获:", e.message);
throw e; // 重新抛出,交给外层处理
}
} catch (e) {
console.log("外部捕获:", e.message);
}
console.error(e)。
Part 2: 自定义错误
内置的 Error 不够用时,继承它创建自己的错误类型
catch 里拿到一个 Error,怎么知道是"用户名太短"还是"网络超时"?如果全靠 message 字符串判断,很快就会乱。更好的做法是创建 ValidationError、AuthError 这样的自定义错误类,用 instanceof 精确区分。再加上 ES2022 的 Error.cause 保留错误链,排查问题时能一路追溯到最底层的原因。
内置错误类型
不同场景抛不同类型的错误
| 错误类型 | 触发场景 |
|---|---|
| SyntaxError | 语法错误,如 JSON.parse("bad") |
| ReferenceError | 访问未声明的变量 |
| TypeError | 对 null 调方法、类型不匹配 |
| RangeError | 数值超出范围 |
| URIError | encodeURI/decodeURI 参数非法 |
所有错误都继承自 Error,都有 name、message、stack 三个属性。
自定义错误类
继承 Error,加上自己的字段
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field; // 自定义字段
}
}
function validateAge(age) {
if (typeof age !== "number") {
throw new ValidationError("age", "年龄必须是数字");
}
if (age < 0 || age > 150) {
throw new ValidationError("age", "年龄超出范围");
}
}
// 使用 instanceof 区分错误类型
try {
validateAge("abc");
} catch (e) {
if (e instanceof ValidationError) {
console.log(`字段 ${e.field} 验证失败: ${e.message}`);
} else {
throw e; // 不认识的错误重新抛出
}
}
Part 3: 异步错误处理
异步代码的错误处理和同步不同,callback、Promise、async/await 各有各的写法
前面学的 try/catch 只能捕获同步错误。setTimeout 里面抛的错、fetch 请求失败,它根本接不住。异步错误要靠回调的 error-first 约定、Promise 的 .catch()、或者 async/await 的 try/catch 来处理。再加上全局错误兜底,即使有漏网之鱼也不会导致应用崩溃。
三种风格对比
callback 靠约定,Promise 靠 catch,async/await 靠 try/catch
// 1. callback 风格:error-first(Node.js 约定)
fs.readFile(path, (err, data) => {
if (err) return callback(err);
callback(null, data);
});
// 2. Promise 风格:链式 .catch()
fetch("/api/data")
.then(res => res.json())
.catch(err => console.error(err))
.finally(() => hideLoading());
// 3. async/await 风格(推荐)
async function fetchData() {
try {
let res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(err);
throw err;
}
}
实际开发中 async/await + try/catch 是主流写法,可读性最好。
全局错误兜底
漏网的错误,用全局监听接住
// 浏览器环境
window.addEventListener("error", (e) => {
console.log("全局错误:", e.message, e.filename, e.lineno);
});
window.addEventListener("unhandledrejection", (e) => {
console.log("未捕获的 Promise 错误:", e.reason);
});
// Node.js 环境
process.on("uncaughtException", (err) => {
console.error("未捕获异常:", err);
});
process.on("unhandledRejection", (reason) => {
console.error("未处理的 Promise 拒绝:", reason);
});
Part 4: 调试技巧
写代码一半时间在写,另一半时间在找 bug,掌握调试工具效率翻倍
与其到处加 console.log 再删掉,不如学会断点调试。在 Sources 面板点一下行号,程序执行到那里就暂停——你能看到所有变量的值、调用栈、甚至手动执行表达式。Node.js 也一样,--inspect 启动后用 Chrome 或 VS Code 接入。再加上 console.table、console.trace 这些高级技巧,定位 bug 的速度能快好几倍。
浏览器 DevTools
Sources 面板设断点,比 console.log 高效十倍
// 方式一:在 Sources 面板点击行号设断点
// 方式二:代码中写 debugger(推荐调试时用)
function complexCalc(data) {
debugger; // 执行到这里会暂停
return data.reduce((a, b) => a + b, 0);
}
| 快捷键 | 功能 |
|---|---|
| F8 | 继续执行 |
| F10 | 步过(不进入函数内部) |
| F11 | 步入(进入函数内部) |
| Shift+F11 | 步出(跳出当前函数) |
Console 技巧
不只是 console.log,还有很多好用的方法
// 样式化输出
console.log("%cStyled", "color: red; font-size: 20px");
// 表格输出——看数组和对象特别清晰
console.table([{name: "A"}, {name: "B"}]);
// 打印调用栈
console.trace("调用栈");
// 计时
console.time("操作");
// ... 一段代码
console.timeEnd("操作"); // 输出耗时
// 分组
console.group("用户信息");
console.log("姓名: 张三");
console.log("年龄: 25");
console.groupEnd();
Node.js 调试
--inspect 打开调试端口,VS Code 或 Chrome 接入
# 启动调试模式
$ node --inspect app.js
# 然后打开 Chrome: chrome://inspect
# 在首行就暂停
$ node --inspect-brk app.js
# 内置命令行调试器
$ node inspect app.js
# n (下一步) c (继续) s (步入) o (步出) repl (交互)
推荐用 VS Code 的调试功能。在项目根目录创建 .vscode/launch.json,然后按 F5 一键调试。
Part 5: 防御性编程
与其出错了再调试,不如写代码时就防止错误发生
错误处理和调试都是"事后补救"。更高级的做法是在写代码时就堵住错误的源头——这就是防御性编程。函数入口校验参数、严格模式让隐式错误显式化、TypeScript 加类型保护、ESLint 做静态检查。多写几行防御代码,调试时能省下十倍的时间。
防御性编程清单
写代码时多想一步,调试时少花十倍时间
输入校验
始终检查函数参数:if (!name) throw new Error(...)
严格模式
"use strict" 让隐式错误变成显式报错。
类型安全
使用 TypeScript 或 JSDoc 增强类型检查。
错误日志
记录错误日志,不吞掉错误,避免空 catch。
全局兜底
添加 uncaughtException 和 unhandledRejection 监听。
静态检查
ESLint 能检测出很多潜在问题,一定要用。
常见错误排查
TypeError: Cannot read property of undefined
最常见错误,没有之一。 说明你在 undefined 上访问了属性。检查链式调用中哪一步返回了 undefined。
UnhandledPromiseRejection
Promise 被 reject 了但没有 .catch()。 给所有 Promise 加上 .catch() 或用 try/catch 包裹 async 函数。
Maximum call stack size exceeded
无限递归。 检查递归函数是否有正确的终止条件。
链式错误
ES2022 的 Error.cause 属性,让错误可以追溯根因
// 在 catch 中包装原始错误,保留根因
function readConfig(path) {
try {
return JSON.parse(fs.readFileSync(path, "utf-8"));
} catch (err) {
throw new Error(`读取配置失败: ${path}`, { cause: err });
}
}
try {
readConfig("./config.json");
} catch (e) {
console.error(e.message); // "读取配置失败: ./config.json"
console.error(e.cause.message); // 原始错误信息
}
// 遍历错误链找到根因
function getRootCause(err) {
let current = err;
while (current.cause) {
current = current.cause;
}
return current;
}
console.log(getRootCause(e).message); // 最底层的错误
{ cause: err } 保留错误链。
错误处理模式
一个完整的 API 错误处理层:自动处理 HTTP 错误、网络错误、超时
const ERROR_MESSAGES = {
NETWORK: "网络连接失败,请检查网络后重试",
TIMEOUT: "请求超时,服务器响应过慢",
400: "请求参数有误,请检查输入",
401: "登录已过期,请重新登录",
403: "没有权限执行此操作",
404: "请求的资源不存在",
500: "服务器开小差了,请稍后再试",
};
class HttpClient {
constructor(baseURL, timeout = 5000) {
this.baseURL = baseURL;
this.timeout = timeout;
}
async request(path, options = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeout);
try {
const res = await fetch(this.baseURL + path, {
...options,
signal: controller.signal,
});
if (!res.ok) {
throw new Error(ERROR_MESSAGES[res.status] || `HTTP ${res.status}`, {
cause: { status: res.status, url: res.url },
});
}
return await res.json();
} catch (err) {
if (err.name === "AbortError") {
throw new Error(ERROR_MESSAGES.TIMEOUT);
}
if (err instanceof TypeError) {
throw new Error(ERROR_MESSAGES.NETWORK);
}
throw err;
} finally {
clearTimeout(timer);
}
}
}
// 使用
const api = new HttpClient("https://api.example.com");
try {
const data = await api.request("/users/1");
} catch (e) {
showToast(e.message); // 用户友好的中文提示
console.error(e); // 开发者查看完整错误
}
试一试
练习一
任务:写一个 safeParse 函数,安全解析 JSON 字符串。解析成功返回数据,失败返回 null 并打印错误。
// 提示:用 try/catch 包裹 JSON.parse
// 你的代码:
参考答案
function safeParse(str) {
try {
return JSON.parse(str);
} catch (e) {
console.error("JSON 解析失败:", e.message);
return null;
}
}
console.log(safeParse('{"name":"张三"}')); // {name: "张三"}
console.log(safeParse("invalid")); // null
练习二
任务:创建一个 AppError 类,包含 code 字段。然后写一个函数模拟用户注册,对用户名长度做验证(3-20 字符)。
// 提示:继承 Error,添加 code 字段
// 你的代码:
参考答案
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = "AppError";
this.code = code;
}
}
function register(username) {
if (!username || username.length < 3) {
throw new AppError("用户名至少3个字符", "NAME_TOO_SHORT");
}
if (username.length > 20) {
throw new AppError("用户名不能超过20字符", "NAME_TOO_LONG");
}
return { success: true, username };
}
try {
register("ab");
} catch (e) {
if (e instanceof AppError) {
console.log(`[${e.code}] ${e.message}`);
}
}
练习三
任务:实现一个 RetryPolicy 类,支持三种重试策略:fixed(固定间隔)、exponential(指数退避)、自定义函数。该类有一个 execute(fn) 方法,接受一个异步函数,失败时按策略重试,最多重试 3 次。
// 提示:用策略模式,sleep 函数辅助延迟
// const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 你的代码:
参考答案
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
class RetryPolicy {
constructor(strategy = "fixed", baseDelay = 1000, maxRetries = 3) {
this.strategy = strategy;
this.baseDelay = baseDelay;
this.maxRetries = maxRetries;
}
getDelay(attempt) {
if (typeof this.strategy === "function") {
return this.strategy(attempt);
}
if (this.strategy === "exponential") {
return this.baseDelay * Math.pow(2, attempt);
}
return this.baseDelay; // fixed
}
async execute(fn) {
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt < this.maxRetries) {
const delay = this.getDelay(attempt);
console.log(`第 ${attempt + 1} 次失败,${delay}ms 后重试...`);
await sleep(delay);
}
}
}
throw lastError;
}
}
// 测试:模拟一个前两次失败、第三次成功的函数
let callCount = 0;
async function unstableApi() {
callCount++;
if (callCount < 3) throw new Error("服务暂时不可用");
return { data: "成功" };
}
// 固定间隔重试
const fixedRetry = new RetryPolicy("fixed", 500);
const result = await fixedRetry.execute(unstableApi);
console.log(result); // { data: "成功" }
// 指数退避重试(500ms -> 1000ms -> 2000ms)
const expRetry = new RetryPolicy("exponential", 500);
// 自定义策略
const customRetry = new RetryPolicy(
(attempt) => [200, 500, 1000][attempt] || 1000
);
📝 要点总结
本章核心知识点
try/catch/finally
捕获异常,确保清理逻辑执行
内置错误
TypeError、SyntaxError、RangeError 等
自定义错误
继承 Error 类,添加业务上下文
调试工具
DevTools 断点、调用栈、变量监视
防御性编程
输入校验、边界检查、优雅降级
下一章
错误处理和调试技能就绪。下一章学习正则表达式基础——用简洁的模式匹配和操作文本。