编写 JavaScript 应用时,错误处理与调试是保障代码质量与用户体验的关键。本文将深入讲解 JavaScript 中的错误类型、捕获机制、异步错误处理、全局错误监听,以及 Chrome DevTools 调试技巧。通过 20+ 个实战代码片段和详细解释,帮助你系统掌握错误处理与调试的核心技能,写出更健壮的代码。
为什么需要错误处理
任何应用都无法保证 100% 无错误。网络波动、用户输入异常、后端返回格式不符等都可能导致程序崩溃。良好的错误处理可以:
- 防止脚本中断执行,提升用户体验
- 提供有意义的错误反馈
- 记录错误信息以便快速修复
- 保证程序状态的一致性
错误类型大全
JS 内置了多种错误构造函数,每种对应特定的异常情况。
| 错误类型 | 说明 | 常见触发场景 |
|---|---|---|
| SyntaxError | 语法错误 | 缺少括号、变量名非法等,代码无法解析 |
| ReferenceError | 引用不存在的变量 | console.log(x) 但 x 未定义 |
| TypeError | 值不是期望的类型 | null.fn() 、 const a=1; a() |
| RangeError | 数值超出有效范围 | 数组长度负数、递归过深 |
| URIError | URI 处理函数传参错误 | decodeURI('%') |
| EvalError | eval() 执行异常(很少用) | 已弃用,保留兼容性 |
try {
console.log(unknownVariable);
} catch (err) {
console.log(err.name); // "ReferenceError"
console.log(err.message); // "unknownVariable is not defined"
}
// TypeError 示例
const obj = null;
obj.toString(); // Uncaught TypeError: Cannot read property 'toString' of null
📌 错误对象属性
所有错误实例都包含三个标准属性:
- name:错误类型名称
- message:错误描述
- stack:调用堆栈(非标准但广泛支持)
console.log(err.stack); // ReferenceError: unknownVariable is not defined // at <anonymous>:3:15 // ...
try...catch 捕获同步错误
try {
// 可能抛出错误的代码
let result = riskyOperation();
console.log('成功:', result);
} catch (error) {
// 处理错误
console.error('出错了:', error.message);
} finally {
// 无论是否出错都会执行
console.log('清理工作');
}
// 只捕获特定类型错误
try {
JSON.parse('invalid json');
} catch (err) {
if (err instanceof SyntaxError) {
console.log('JSON 语法错误');
} else {
throw err; // 重新抛出其他错误
}
}
🧪 finally 典型应用
释放资源、关闭连接、隐藏加载提示等操作放在 finally 中保证执行。
let loading = true;
try {
// 网络请求
} catch (e) {
// 显示错误
} finally {
loading = false; // 无论如何都关闭loading
}
主动抛出错误 throw
使用 throw 可以抛出自定义错误,中断程序并进入最近的 catch 块。
throw '发生错误'; // 但无法获取堆栈信息
// 推荐抛出 Error 对象
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');
}
return a / b;
}
try {
divide(10, 0);
} catch (err) {
console.log(err.message); // "除数不能为0"
}
📦 自定义错误类
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
throw new ValidationError("邮箱格式错误");
异步错误处理
try...catch 无法捕获 setTimeout、Promise 中抛出的错误(除非使用 async/await)。
回调时代的错误处理
fs.readFile('file.txt', (err, data) => {
if (err) {
console.error('读取失败', err);
return;
}
console.log(data);
});
Promise 错误处理
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error('请求失败:', err));
// 也可以捕获 then 中的异常
Promise.resolve().then(() => {
throw new Error('then 内错误');
}).catch(e => console.log(e.message));
⚡ async/await 搭配 try/catch
async function getData() {
try {
const res = await fetch('/data');
const json = await res.json();
return json;
} catch (err) {
console.error('捕获异步错误:', err);
// 可以返回默认值或重新抛出
}
}
未捕获的 Promise 拒绝
没有 catch 的 Promise 拒绝会触发 unhandledrejection 事件。
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止默认控制台警告
});
全局错误捕获
使用 window.onerror 或 addEventListener('error') 捕获运行时错误(含语法错误?不,语法错误无法被捕获)。
window.onerror = function(msg, url, line, col, error) {
console.log('全局错误:', msg, 'at', line);
return true; // 阻止默认错误处理
};
// 现代方式(推荐)
window.addEventListener('error', (event) => {
console.log('捕获错误:', event.error);
// 可以上报到监控系统
});
📡 资源加载错误
图片、脚本加载失败也会触发 error 事件,可以通过捕获处理:
window.addEventListener('error', (e) => {
if (e.target.tagName === 'IMG') {
console.log('图片加载失败:', e.target.src);
}
}, true); // 捕获阶段
调试利器:console 对象
除了常用的 console.log,还有多种实用方法。
console.info('提示信息');
console.warn('警告信息');
console.error('错误信息');
// 表格展示
console.table([{name:'Tom', age:20}, {name:'Jerry', age:18}]);
// 分组
console.group('请求详情');
console.log('url: /api');
console.log('status: 200');
console.groupEnd();
// 计时
console.time('循环耗时');
for(let i=0;i<1000000;i++){}
console.timeEnd('循环耗时');
🧹 清理与断言
// 条件断言
const value = 5;
console.assert(value > 10, '值应该大于10'); // 不输出
// 堆栈追踪
function foo() { bar(); }
function bar() { console.trace(); }
foo(); // 打印调用栈
debugger 与浏览器断点
在代码中插入 debugger; 语句,当打开 DevTools 时,执行会自动暂停在该行。
debugger; // 执行到此处会暂停(如果 DevTools 开启)
return price * count;
}
calculateTotal(100, 3);
此外,可以在 Sources 面板手动添加断点、条件断点、DOM 断点等,是定位复杂问题的利器。
Source Map 调试生产代码
生产环境的代码通常经过压缩和混淆,错误堆栈难以阅读。Source Map 可以将压缩代码映射回原始源码。
// devtool: 'source-map'
// 错误监控平台利用 source map 还原原始位置
// 例如 Sentry 支持上传 source map
在 Chrome DevTools 中,只要 .map 文件可访问,会自动还原源码,便于调试线上问题。
典型错误案例解析
Uncaught TypeError: Cannot read property 'xxx' of undefined
const user = { profile: { name: '张三' } };
console.log(user.profile.age.toFixed()); // profile.age 是 undefined
// 安全访问方式
console.log(user.profile?.age?.toFixed?.()); // 可选链
异步回调中的错误被吞没
try {
setTimeout(() => {
throw new Error('异步错误');
}, 100);
} catch (e) { // 无法捕获
console.log('不会执行');
}
// 正确:在回调内部捕获
setTimeout(() => {
try { throw new Error('异步错误'); } catch(e) {}
}, 100);
错误处理最佳实践
- 区分操作错误和程序员错误:操作错误(如网络超时)应优雅处理;程序员错误(如传错参数)应尽快失败并修复。
- 始终捕获异步错误:使用 async/await + try/catch 或 Promise.catch()。
- 不要吞掉错误:空的 catch 块会使调试困难,至少记录日志。
- 抛出有意义的错误消息:便于快速定位。
- 集中错误监控:使用 Sentry、Fundebug 等工具收集生产环境错误。
- 为自定义错误设置正确的 name 和 stack:继承 Error 而不是单纯抛出对象。
class HttpError extends Error {
constructor(message, status) {
super(message);
this.name = 'HttpError';
this.status = status;
}
}
错误处理与调试是 JavaScript 工程化的重要基石。掌握本文内容后,你将能更从容地应对运行时异常,并利用现代工具链快速定位和修复问题。