第5章 5.2 错误处理与 try/catch
上一章我们折腾完模块化的问题,你已经能让代码「想用哪个用哪个」了。但你有没有想过——万一某行代码突然罢工了怎么办?用户输入了个乱七八糟的东西怎么办?网络请求突然超时了怎么办?
这一章,我们来解决「代码出错了怎么办」这个问题。
你有没有遇到过这种情况:写了一个程序,自己测试的时候好好的,一给别人用,各种奇怪的报错就冒出来了——什么「undefined is not a function」「Cannot read property 'xxx' of null」「JSON.parse 报错」……
这些就是 JavaScript 世界里的「意外」。今天这章,教你怎么优雅地处理这些意外,让你的程序即使遇到问题也不会直接崩溃死给你看。
🎯 开场 3 分钟:为什么要学这个?
真实场景
想象你在经营一家餐厅。顾客点了一份牛排,你把单子传给后厨——结果后厨发现没牛肉了。
不靠谱的处理方式:厨师直接撂挑子不干了,整个餐厅停业。
靠谱的处理方式:厨师告诉服务员「牛排没\n\n
\n\n
\n\n了,换成猪排行不行?」,服务员转告顾客,顾客说行,那就上猪排。
编程也一样。你的代码遇到意外(没牛肉),不能直接崩溃(整个餐厅停业),而是要 try 一下(尝试做),catch 一下(出问题了接手),最后 finally 一下(不管成不成都要收拾台面)。
痛点问题
你是不是遇到过:
- 程序跑着跑着突然报错,整个页面直接空白
- 用户填表单少打了个 @ 符号,整个功能报废
- 读取文件时代码报错,不知道哪里出了问题
学完这章,这些问题都能优雅解决。
🧱 基础 25 分钟:核心概念
5.2.1 为什么要用 try/catch?
是什么:try/catch 是一种「保护机制」,让你的代码在遇到错误时不至于直接崩溃。
生活类比:就像汽车的安全气囊,不是在你正常开车的时候弹出,而是万一发生碰撞时保护你。
为什么用:没有 try/catch,代码遇到错误就会直接停止执行,后面的代码永远不会跑。有 try/catch,你可以「捕获」错误,然后决定怎么handle它。
// 假设这是一个可能出错的代码
try {
let data = JSON.parse('{"name": "小明"}'); // 正常JSON,能解析
console.log(data.name); // 输出: 小明
} catch (error) {
console.log('出错了:', error.message); // 这行不会执行
}
// 再来一个会出错的
try {
let badData = JSON.parse('这不是合法的JSON'); // 这里会报错
console.log(badData.name); // 不会执行到这行
} catch (error) {
console.log('出错了:', error.message); // 输出: Unexpected token '这'...
}
解释:第一段代码正常执行,第二段代码的 JSON 解析失败,但没有被「炸掉」,而是被 catch 捕获了。
5.2.2 try/catch/finally 三兄弟
try {
// 尝试执行的代码
console.log('开始执行...');
let result = riskyFunction(); // 可能出问题的函数
console.log('执行成功:', result);
} catch (error) {
// 出问题了就会执行这里
console.log('捕获到错误:', error.message);
} finally {
// 不管成功还是失败,都会执行这里
console.log('清理工作完成');
}
三兄弟分工:
- try:勇敢的尝试者,「我来试试这段代码能不能跑」
- catch:救火队员,「try 那边出事了?我来处理」
- finally:忠实的保洁阿姨,「不管前面怎么样,最后的卫生我肯定要打扫」
5.2.3 Error 对象是什么?
当你 catch 一个错误时,catch 后面括号里的 error 是个对象,包含了错误的详细信息:
try {
// 人为抛出一个错误
throw new Error('服务器连接失败');
} catch (error) {
console.log('错误名称:', error.name); // Error
console.log('错误消息:', error.message); // 服务器连接失败
console.log('错误堆栈:', error.stack); // 详细的错误位置信息
}
error 对象的几个常用属性:
- message:人类可读的错误描述
- name:错误的类型(Error, TypeError, ReferenceError 等)
- stack:完整的错误堆栈信息,帮你定位问题在哪
5.2.4 常见的错误类型
JavaScript 里有很多内置的错误类型:
// SyntaxError:语法错误,写代码的时候就报
try {
eval('const a = '); // 语法错误
} catch (e) {
console.log('类型:', e.name); // SyntaxError
}
// ReferenceError:引用了一个不存在的变量
try {
console.log(undefinedVariable); // 没定义过
} catch (e) {
console.log('类型:', e.name); // ReferenceError
}
// TypeError:类型错误
try {
let num = 123;
num.split(); // 数字没有 split 方法
} catch (e) {
console.log('类型:', e.name); // TypeError
}
// RangeError:值超出范围
try {
let arr = new Array(-1); // 数组长度不能是负数
} catch (e) {
console.log('类型:', e.name); // RangeError
}
5.2.5 如何主动抛出错误(throw)
是什么:throw 就像你主动按下的「紧急报警按钮」,告诉程序「这里出问题了」。
为什么要用:有时候你发现数据不对,但 JavaScript 不会自动报错,这时候你可以主动 throw 一个错误。
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0'); // 主动抛出错误
}
return a / b;
}
try {
let result = divide(10, 0); // 这里会抛出错误
} catch (error) {
console.log('捕获到错误:', error.message); // 除数不能为0
}
// 正常情况
try {
let result = divide(10, 2);
console.log('计算结果:', result); // 5
} catch (error) {
console.log('捕获到错误:', error.message);
}
5.2.6 自定义错误类型
有时候内置的错误类型不够用,你可以创建自己的错误类型:
// 定义一个自定义错误
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError'; // 错误名称
}
}
// 使用自定义错误
function validateAge(age) {
if (typeof age !== 'number') {
throw new ValidationError('年龄必须是数字');
}
if (age < 0 || age > 150) {
throw new ValidationError('年龄必须在 0-150 之间');
}
return '年龄合法';
}
try {
console.log(validateAge(25)); // 正常:年龄合法
console.log(validateAge(-5)); // 抛出 ValidationError
} catch (error) {
if (error instanceof ValidationError) {
console.log('验证失败:', error.message);
} else {
console.log('其他错误:', error.message);
}
}
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):温度转换器 with 错误处理
场景:做一个摄氏度和华氏度互相转换的工具,用户可能输入乱七八糟的东西。
// 温度转换器 - 带错误处理
function celsiusToFahrenheit(celsius) {
if (typeof celsius !== 'number' || isNaN(celsius)) {
throw new Error('请输入有效的数字');
}
return (celsius * 9/5) + 32;
}
function fahrenheitToCelsius(fahrenheit) {
if (typeof fahrenheit !== 'number' || isNaN(fahrenheit)) {
throw new Error('请输入有效的数字');
}
return (fahrenheit - 32) * 5/9;
}
// 测试用例
function convert(type, value) {
try {
if (type === 'CtoF') {
const result = celsiusToFahrenheit(value);
console.log(`${value}°C = ${result.toFixed(2)}°F`);
} else if (type === 'FtoC') {
const result = fahrenheitToCelsius(value);
console.log(`${value}°F = ${result.toFixed(2)}°C`);
}
} catch (error) {
console.log(`转换失败: ${error.message}`);
} finally {
console.log('--- 本次转换结束 ---');
}
}
// 测试
convert('CtoF', 100); // 正常: 100°C = 212.00°F
convert('CtoF', 'hello'); // 错误: 请输入有效的数字
convert('FtoC', 32); // 正常: 32°F = 0.00°C
预期输出:
100°C = 212.00°F
--- 本次转换结束 ---
转换失败: 请输入有效的数字
--- 本次转换结束 ---
32°F = 0.00°C
--- 本次转换结束 ---
一句话解释:用 throw 主动抛出错误,try/catch 捕获并友好地提示用户。
项目 2(15 分钟):JSON 配置文件读取器
场景:读取一个 JSON 配置文件,解析配置,如果格式不对就给出友好提示。
// 模拟从文件读取的配置数据(实际项目中可能是 fs.readFileSync)
const configData = `{
"appName": "我的工具箱",
"version": "1.0.0",
"maxUsers": 100,
"theme": "dark",
"features": ["导入", "导出", "同步"]
}`;
// 模拟读取失败的配置数据
const badConfigData = `{这不是合法的JSON}`;
function parseConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
// 验证配置项
if (!config.appName) {
throw new Error('配置缺少 appName 字段');
}
if (!config.version) {
throw new Error('配置缺少 version 字段');
}
if (typeof config.maxUsers !== 'number') {
throw new Error('maxUsers 必须是数字');
}
return config;
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`配置文件格式错误: ${error.message}`);
}
throw error;
}
}
function displayConfig(config) {
console.log('='.repeat(30));
console.log(`应用名称: ${config.appName}`);
console.log(`版本号: ${config.version}`);
console.log(`最大用户数: ${config.maxUsers}`);
console.log(`功能列表: ${config.features.join(', ')}`);
console.log('='.repeat(30));
}
// 正常配置
try {
const config = parseConfig(configData);
displayConfig(config);
} catch (error) {
console.log(`读取配置失败: ${error.message}`);
} finally {
console.log('配置读取操作完成\n');
}
// 错误配置
try {
const badConfig = parseConfig(badConfigData);
displayConfig(badConfig);
} catch (error) {
console.log(`读取配置失败: ${error.message}`);
} finally {
console.log('配置读取操作完成');
}
预期输出:
==============================
应用名称: 我的工具箱
版本号: 1.0.0
最大用户数: 100
功能列表: 导入, 导出, 同步
==============================
配置读取操作完成
读取配置失败: 配置文件格式错误: Unexpected token '这'...
配置读取操作完成
一句话解释:JSON.parse 可能抛出 SyntaxError,我们 catch 后重新包装成更友好的错误消息。
项目 3(15 分钟):待办事项管理器(with 持久化)
场景:做一个命令行待办事项管理器,能添加、查看、删除事项,数据保存在本地 JSON 文件(模拟)。
// 待办事项管理器 v1.0
class TodoList {
constructor() {
this.todos = [];
}
// 添加待办事项
add(text) {
if (!text || typeof text !== 'string') {
throw new Error('待办事项内容不能为空');
}
if (text.trim().length === 0) {
throw new Error('待办事项内容不能全是空格');
}
const todo = {
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date().toISOString()
};
this.todos.push(todo);
return todo;
}
// 查看所有待办
list() {
if (this.todos.length === 0) {
console.log('暂无待办事项');
return;
}
this.todos.forEach((todo, index) => {
const status = todo.completed ? '✓' : '○';
const text = todo.completed ? `[完成] ${todo.text}` : `[待办] ${todo.text}`;
console.log(`${index + 1}. ${status} ${text}`);
});
}
// 标记完成
complete(id) {
const todo = this.todos.find(t => t.id === id);
if (!todo) {
throw new Error(`找不到 ID 为 ${id} 的待办事项`);
}
todo.completed = true;
return todo;
}
// 删除待办
remove(id) {
const index = this.todos.findIndex(t => t.id === id);
if (index === -1) {
throw new Error(`找不到 ID 为 ${id} 的待办事项`);
}
const removed = this.todos.splice(index, 1)[0];
return removed;
}
// 模拟保存到文件
save() {
try {
const data = JSON.stringify(this.todos, null, 2);
console.log('保存数据:', data);
return true;
} catch (error) {
throw new Error(`保存失败: ${error.message}`);
}
}
// 模拟从文件加载
load(jsonData) {
try {
this.todos = JSON.parse(jsonData);
if (!Array.isArray(this.todos)) {
throw new Error('数据格式错误');
}
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`数据损坏: ${error.message}`);
}
throw error;
}
}
}
// 使用示例
const todoList = new TodoList();
try {
// 添加一些待办
todoList.add('学习 JavaScript 错误处理');
todoList.add('完成作业');
todoList.add('给妈妈打电话');
console.log('--- 当前待办列表 ---');
todoList.list();
// 标记一个完成
todoList.complete(todoList.todos[0].id);
// 再添加一个测试错误处理
todoList.add(''); // 会报错
} catch (error) {
console.log(`操作失败: ${error.message}`);
} finally {
console.log('--- 操作结束 ---\n');
}
// 测试加载损坏的数据
try {
todoList.load('这不是合法的JSON数据');
} catch (error) {
console.log(`加载失败: ${error.message}`);
} finally {
console.log('--- 加载操作结束 ---');
}
预期输出:
--- 当前待办列表 ---
1. ○ [待办] 学习 JavaScript 错误处理
2. ○ [待办] 完成作业
3. ○ [待办] 给妈妈打电话
操作失败: 待办事项内容不能为空
--- 操作结束 ---
加载失败: 数据损坏: Unexpected token '这'...
--- 加载操作结束 ---
一句话解释:把错误处理融入到业务逻辑里,每一步都可能有风险,每一步都用 try/catch 保护。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:catch 里的变量名不要乱起
// ❌ 错误示例:catch 里的 error 被不小心改掉了
try {
riskyOperation();
} catch (error) {
console.log(error.message);
error = new Error('新的错误'); // 报语法错误!error 是个 const
}
// ✅ 正确示例
try {
riskyOperation();
} catch (err) { // 换个名字也行
console.log(err.message);
let newError = new Error('新的错误'); // 用新变量
}
坑 2:finally 里不要放改变流程的代码
// ❌ 错误示例:finally 里的 return 会覆盖 try 里的返回值
function badExample() {
try {
return 1;
} catch (e) {
return 2;
} finally {
return 3; // 这会覆盖前面的 return,最终返回 3
}
}
// ✅ 正确示例
function goodExample() {
try {
return 1;
} catch (e) {
return 2;
} finally {
console.log('我只是清理,不会改变返回值'); // 只是打印
}
// return 1 或 return 2
}
坑 3:try/catch 不是万能的,别什么都包
// ❌ 错误示例:语法错误 try/catch 捕获不了
try {
eval('const a = '); // SyntaxError 在解析阶段就报错了
} catch (e) {
console.log('不会执行到这'); // 因为代码还没运行就报错了
}
// ✅ 正确示例:语法错误只能在 IDE/lint 阶段发现
// 运行时不要写语法错误的代码
坑 4:异步代码的错误处理
// ❌ 错误示例:try/catch 捕获不了异步错误
try {
setTimeout(() => {
throw new Error('异步错误'); // 这个 catch 捕获不到
}, 100);
} catch (e) {
console.log('捕获不到'); // 永远不会执行
}
// ✅ 正确示例:用 Promise 的 catch 方法
new Promise((resolve, reject) => {
reject(new Error('异步错误'));
})
.then(result => console.log(result))
.catch(error => console.log('捕获到:', error.message)); // 正确
坑 5:catch 了错误但什么都不做
// ❌ 错误示例:吞掉错误,不知道出了什么问题
try {
riskyOperation();
} catch (e) {
// 什么都不做,以后 Debug 会哭
}
// ✅ 正确示例:至少记录下来
try {
riskyOperation();
} catch (error) {
console.error('操作失败:', error.message);
// 或者记录到日志文件
// throw error; // 如果需要继续抛出也行
}
性能小贴士
不要用 try/catch 做流程控制。try/catch 有性能开销,如果你的逻辑能提前判断,就不要依赖异常:
// ❌ 低性能:用 try/catch 检查类型
function isValidNumber(value) {
try {
Number(value);
return true;
} catch {
return false;
}
}
// ✅ 高性能:直接用 isNaN
function isValidNumber(value) {
return !isNaN(value) && typeof value === 'number';
}
调试技巧
用 console.error 而不是 console.log 来打印错误:
try {
riskyOperation();
} catch (error) {
console.error('出错了:', error); // 错误信息用 console.error
console.log('但程序继续运行'); // 普通信息用 console.log
}
用 Error 对象的 stack 属性定位问题:
try {
functionA();
} catch (error) {
console.log('错误堆栈:');
console.log(error.stack);
// 会显示完整的调用链:A 调用 B,B 调用 C,C 报错了
}
✏️ 练习题
练习 1(2 分钟):修复除法函数
// 输入:调用 divide(10, 0)
// 预期输出:捕获到错误: 除数不能为0
// 提示:参考项目 1 的 divide 函数
练习 2(2 分钟):添加参数验证
// 在项目 1 的 convert 函数里,加一个判断
// 如果 type 不是 'CtoF' 也不是 'FtoC',抛出错误
// 预期:convert('abc', 100) 输出 "不支持的转换类型"
练习 3(3 分钟):处理数组越界
// 写一个函数 getElement(arr, index)
// 如果 index 超出数组范围,抛出 "索引越界" 错误
// 输入:getElement([1,2,3], 10)
// 输出:捕获到错误: 索引越界
练习 4(5 分钟):JSON 数据验证
// 用项目 2 的 parseConfig 函数
// 验证这段配置,思考会报什么错:
const data = `{"appName": "测试", "maxUsers": "不是数字"}`;
练习 5(5 分钟):分析错误堆栈
// 假设你看到了这个错误堆栈:
// Error: 数据损坏
// at TodoList.load (<anonymous>:18:25)
// at <anonymous>:45:8
// 请回答:报错发生在哪一行?是什么类型的错误?
作业:做一个「数据验证工具」
需求描述:做一个数据验证小工具,能验证用户注册信息。
功能点:
1. 验证用户名(不能为空,不能超过 20 字符,不能包含特殊字符)
2. 验证邮箱(必须是合法邮箱格式)
3. 验证密码(长度至少 8 位,必须包含数字和字母)
加分项:
1. 用自定义错误类型 ValidationError
2. 支持批量验证,返回所有错误而不是遇到第一个就停
验收标准:
- 能跑起来
- 输入错误数据时给出友好提示
- 代码有注释
📚 总结 + 资源
本章 3 个核心点
- try/catch/finally 是 JavaScript 的「安全气囊」,让你的程序遇到错误也不崩溃
- throw 让你能主动「按报警按钮」,把业务逻辑中的异常情况通知出去
- 自定义错误类型 让错误分类更清晰,方便不同地方做不同处理
延伸学习资源
- MDN 官方文档 - try...catch
- 《JavaScript 高级程序设计》第 4 章:变量、作用域和内存问题(里面讲了很多错误处理)
- 视频:B 站「技术蛋老师」的错误处理专题
互动钩子
你在写代码的时候遇到过什么奇葩报错?是被 try/catch 救了一命,还是踩了 try/catch 的坑?评论区聊聊,老粉优先回复!
下章预告:下一章我们要学一个「超级瑞士军刀」——正则表达式。有了它,处理字符串就像开了挂一样,验证邮箱、提取数据、替换文字,全都不在话下。

评论(0)