JavaScript 异常处理
简介
异常(Exception)是程序运行时发生的错误。JavaScript 提供了 try...catch...finally 语句和 throw 语句来处理异常,防止程序因错误而崩溃。
try…catch 语句
基本语法
try {
// 尝试执行的代码
} catch (error) {
// 发生错误时执行的代码
}
执行流程
- 执行
try块中的代码 - 如果没有错误,跳过
catch块 - 如果发生错误,停止执行
try块中的剩余代码,跳转到catch块 catch块接收错误对象作为参数
示例
try {
let result = JSON.parse('不是有效的 JSON');
console.log(result); // 这行不会执行
} catch (error) {
console.log('捕获到错误:', error.message);
}
// 输出:捕获到错误: Unexpected token 不 in JSON at position 0
console.log('程序继续运行...'); // 不会被影响
catch 块中的错误对象
catch 块接收一个错误对象,通常包含以下属性:
try {
undefinedFunction(); // 调用不存在的函数
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedFunction is not defined"
console.log(error.stack); // 完整的堆栈跟踪(用于调试)
}
finally 子句
语法
try {
// 尝试执行的代码
} catch (error) {
// 发生错误时执行的代码
} finally {
// 无论是否出错,都会执行的代码
}
特点
finally 块无论是否发生错误,都会执行。即使有 return 语句,也会先执行 finally。
try {
console.log('try 块');
throw new Error('出错了');
} catch (error) {
console.log('catch 块:', error.message);
} finally {
console.log('finally 块(总是执行)');
}
// 输出:
// try 块
// catch 块: 出错了
// finally 块(总是执行)
即使 return 也会执行
function test() {
try {
return 'try 的返回值';
} finally {
console.log('finally 执行了');
}
}
let result = test();
// 输出:finally 执行了
console.log(result); // "try 的返回值"
使用场景
finally 常用于清理资源,如关闭文件、断开数据库连接等。
let file = null;
try {
file = openFile('data.txt');
processFile(file);
} catch (error) {
console.error('处理文件出错:', error.message);
} finally {
if (file) {
closeFile(file); // 确保文件被关闭
console.log('文件已关闭');
}
}
throw 语句
语法
throw 用于主动抛出一个异常。
throw expression;
抛出不同类型的值
// 抛出字符串(不推荐)
try {
throw '发生了一个错误';
} catch (e) {
console.log(e); // "发生了一个错误"
}
// 抛出数字(不推荐)
try {
throw 404;
} catch (e) {
console.log('错误码:', e); // 错误码: 404
}
// 抛出对象(不推荐)
try {
throw { code: 500, message: '服务器错误' };
} catch (e) {
console.log(e.message);
}
最佳实践:抛出 Error 对象
// 推荐:使用 Error 对象
try {
let age = -5;
if (age < 0) {
throw new Error('年龄不能为负数');
}
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "年龄不能为负数"
}
配合 try…catch 使用
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // 抛出错误
} catch (error) {
console.log('错误:', error.message); // 错误: 除数不能为零
}
Error 对象
内置属性
| 属性 | 说明 |
|---|---|
name |
错误类型名称 |
message |
错误信息 |
stack |
堆栈跟踪(用于调试) |
try {
throw new Error('测试错误');
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "测试错误"
console.log(error.stack); // 完整的堆栈信息
}
内置错误类型
JavaScript 提供了多种内置错误类型:
| 错误类型 | 触发场景 |
|---|---|
Error |
通用错误(基类) |
SyntaxError |
语法错误(通常在代码解析时) |
ReferenceError |
引用不存在的变量 |
TypeError |
类型错误(如调用不存在的方法) |
RangeError |
数值超出有效范围 |
URIError |
URI 处理函数使用不当 |
EvalError |
与 eval() 相关的错误(较少见) |
示例:各种内置错误
// SyntaxError:语法错误(通常无法被 try...catch 捕获,因为代码解析失败)
// try {
// eval('alert("Hello"'); // 缺少闭合括号
// } catch (e) {
// console.log(e.name); // "SyntaxError"
// }
// ReferenceError:引用不存在的变量
try {
console.log(nonExistentVar);
} catch (e) {
console.log(e.name); // "ReferenceError"
}
// TypeError:类型错误
try {
let num = 123;
num(); // 尝试把数字当函数调用
} catch (e) {
console.log(e.name); // "TypeError"
}
// RangeError:数值超出范围
try {
let arr = new Array(-1); // 无效的长度
} catch (e) {
console.log(e.name); // "RangeError"
}
// URIError:URI 错误
try {
decodeURIComponent('%'); // 无效的 URI 编码
} catch (e) {
console.log(e.name); // "URIError"
}
自定义错误
通过继承 Error 创建自定义错误
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError'; // 设置错误名称
this.status = 400; // 自定义属性
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} 未找到`);
this.name = 'NotFoundError';
this.status = 404;
}
}
使用自定义错误
function findUser(id) {
let users = { 1: 'Alice', 2: 'Bob' };
if (!users[id]) {
throw new NotFoundError(`用户 ID ${id}`);
}
return users[id];
}
try {
console.log(findUser(3));
} catch (error) {
if (error instanceof NotFoundError) {
console.log('404:', error.message);
console.log('状态码:', error.status);
} else {
console.log('其他错误:', error.message);
}
}
// 输出:404: 用户 ID 3 未找到
// 状态码: 404
多层自定义错误继承
class AppError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class DatabaseError extends AppError {
constructor(message) {
super(message);
this.status = 500;
}
}
class ConnectionError extends DatabaseError {
constructor() {
super('数据库连接失败');
}
}
try {
throw new ConnectionError();
} catch (error) {
console.log(error.name); // "ConnectionError"
console.log(error.status); // 500
console.log(error.message); // "数据库连接失败"
}
异步代码中的异常处理
Promise 的 .catch() 方法
// Promise 链中的错误捕获
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('网络请求失败'));
}, 1000);
});
}
fetchData()
.then(data => console.log('数据:', data))
.catch(error => console.log('捕获到错误:', error.message));
// 1秒后输出:捕获到错误: 网络请求失败
async/await 中的 try…catch
async function getData() {
try {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
} catch (error) {
console.log('请求失败:', error.message);
return null;
}
}
getData();
未捕获的 Promise 错误
// 危险:未捕获的 Promise 错误
// let promise = new Promise((resolve, reject) => {
// throw new Error('未捕获的错误');
// });
// 在浏览器控制台中会显示 "Uncaught (in promise) Error: 未捕获的错误"
// 解决方法1:使用 .catch()
let promise = new Promise((resolve, reject) => {
throw new Error('已捕获的错误');
});
promise.catch(error => console.log('捕获:', error.message));
// 解决方法2:使用 try...catch(在 async 函数中)
async function test() {
try {
let result = await new Promise((resolve, reject) => {
reject(new Error('已捕获'));
});
} catch (error) {
console.log('捕获:', error.message);
}
}
test();
全局捕获未处理的 Promise 错误
// 浏览器中
// window.addEventListener('unhandledrejection', event => {
// console.log('未处理的 Promise 错误:', event.reason);
// });
// Node.js 中
// process.on('unhandledRejection', (reason, promise) => {
// console.log('未处理的 Promise 错误:', reason);
// });
嵌套 try…catch
基本示例
try {
console.log('外层 try');
try {
console.log('内层 try');
throw new Error('内层错误');
} catch (innerError) {
console.log('内层 catch:', innerError.message);
throw new Error('重新抛出给外层'); // 重新抛出
}
} catch (outerError) {
console.log('外层 catch:', outerError.message);
} finally {
console.log('外层 finally');
}
// 输出:
// 外层 try
// 内层 try
// 内层 catch: 内层错误
// 外层 catch: 重新抛出给外层
// 外层 finally
使用场景:分层错误处理
function processOrder(order) {
try {
validateOrder(order); // 可能抛出验证错误
try {
chargePayment(order); // 可能抛出支付错误
fulfillOrder(order); // 可能抛出履约错误
} catch (paymentError) {
console.log('支付失败:', paymentError.message);
cancelOrder(order);
throw paymentError; // 重新抛出,让调用者知道
}
} catch (validationError) {
console.log('订单验证失败:', validationError.message);
throw validationError;
}
}
错误传播
错误会沿着调用栈向上传播
function level3() {
throw new Error('第三层出错');
}
function level2() {
level3(); // 错误从这里传播出去
}
function level1() {
level2();
}
try {
level1();
} catch (error) {
console.log('捕获到错误:', error.message);
console.log('发生位置:', error.stack);
}
// 输出:捕获到错误: 第三层出错
// 堆栈会显示完整的调用链:level1 → level2 → level3
捕获后重新抛出
function process() {
try {
riskyOperation();
} catch (error) {
console.log('记录错误:', error.message);
// 添加额外信息后重新抛出
error.context = '处理用户数据';
throw error; // 重新抛出,让上层处理
}
}
try {
process();
} catch (error) {
console.log('最终处理:', error.context, error.message);
}
最佳实践
1. 不要过度使用 try…catch
// 不推荐:包裹所有代码
try {
let x = 1;
let y = 2;
let sum = x + y;
} catch (e) {
// 这里不会发生错误,try...catch 是多余的
}
// 推荐:只在可能出错的地方使用
try {
let data = JSON.parse(jsonString); // 可能抛出语法错误
} catch (e) {
console.log('JSON 解析失败:', e.message);
}
2. 区分可恢复和不可恢复的错误
// 可恢复的错误:捕获并处理
try {
let config = JSON.parse(localStorage.getItem('config') || '{}');
} catch (e) {
console.log('配置解析失败,使用默认配置');
config = { theme: 'light' };
}
// 不可恢复的错误:抛出或重新抛出
function criticalOperation() {
if (!database.isConnected()) {
throw new Error('数据库未连接'); // 让调用者决定如何处理
}
}
3. 使用具体的错误类型
// 不推荐:都使用通用的 Error
try {
validateInput(input);
} catch (e) {
console.log('出错了'); // 不知道是什么错误
}
// 推荐:使用具体的错误类型或自定义错误
try {
validateInput(input);
} catch (e) {
if (e instanceof ValidationError) {
console.log('验证失败:', e.message);
} else if (e instanceof DatabaseError) {
console.log('数据库错误:', e.message);
} else {
throw e; // 不认识的错误,重新抛出
}
}
4. 异步代码的错误处理
// Promise 链:使用 .catch()
fetchData()
.then(process)
.then(display)
.catch(error => {
console.log('整个链中任何环节出错都会被捕获');
});
// async/await:使用 try...catch
async function main() {
try {
let data = await fetchData();
let result = process(data);
display(result);
} catch (error) {
console.log('出错:', error.message);
}
}
5. finally 用于清理
let connection;
try {
connection = connectToDB();
doWork(connection);
} catch (error) {
logError(error);
throw error;
} finally {
if (connection) {
connection.close(); // 确保连接被关闭
}
}
综合示例
示例 1:表单验证器(使用自定义错误)
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
function validateUser(user) {
if (!user.name || user.name.trim() === '') {
throw new ValidationError('name', '姓名不能为空');
}
if (!user.email || !user.email.includes('@')) {
throw new ValidationError('email', '邮箱格式不正确');
}
if (user.age !== undefined && (user.age < 0 || user.age > 150)) {
throw new ValidationError('age', '年龄必须在 0-150 之间');
}
}
// 使用
let users = [
{ name: 'Alice', email: '[email protected]', age: 25 },
{ name: '', email: '[email protected]' },
{ name: 'Charlie', email: 'invalid' }
];
users.forEach((user, index) => {
try {
validateUser(user);
console.log(`用户 ${index + 1} 验证通过`);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`用户 ${index + 1} 验证失败 [${error.field}]: ${error.message}`);
} else {
console.log(`用户 ${index + 1} 发生未知错误:`, error);
}
}
});
// 输出:
// 用户 1 验证通过
// 用户 2 验证失败 [name]: 姓名不能为空
// 用户 3 验证失败 [email]: 邮箱格式不正确
示例 2:带重试的异步操作
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`尝试第 ${attempt} 次...`);
let response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`第 ${attempt} 次失败: ${error.message}`);
if (attempt === maxRetries) {
throw new Error(`请求失败,已重试 ${maxRetries} 次`);
}
// 等待一段时间后重试(指数退避)
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// 使用
// fetchWithRetry('https://api.example.com/data')
// .then(data => console.log('成功:', data))
// .catch(error => console.log('最终失败:', error.message));
示例 3:资源安全的文件处理
// Node.js 示例
const fs = require('fs');
function processFileSafely(filePath) {
let fileHandle;
try {
console.log('打开文件...');
fileHandle = fs.openSync(filePath, 'r');
let buffer = Buffer.alloc(1024);
let bytesRead = fs.readSync(fileHandle, buffer, 0, 1024, null);
let content = buffer.toString('utf8', 0, bytesRead);
console.log('文件内容:', content);
return content;
} catch (error) {
console.log('处理文件出错:', error.message);
return null;
} finally {
if (fileHandle) {
console.log('关闭文件...');
fs.closeSync(fileHandle);
}
}
}
// processFileSafely('test.txt');
示例 4:错误边界(模拟 React 的概念)
function createErrorBoundary(fn, fallback = null) {
return function(...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.log('组件出错,使用降级方案:', error.message);
return fallback;
}
};
}
// 使用
function riskyComponent(data) {
if (!data) throw new Error('没有数据');
return `数据显示: ${data}`;
}
let safeComponent = createErrorBoundary(riskyComponent, '暂无数据');
console.log(safeComponent('Hello')); // 数据显示: Hello
console.log(safeComponent(null)); // 暂无数据(没有崩溃)
总结:异常处理速查表
| 语法/概念 | 说明 |
|---|---|
try { } catch(e) { } |
捕获同步错误 |
try { } catch(e) { } finally { } |
捕获错误并清理资源 |
throw value |
抛出错误 |
new Error(msg) |
创建错误对象 |
catch(e) { if (e instanceof X) } |
根据错误类型处理 |
Promise.catch() |
捕获 Promise 错误 |
async/await + try...catch |
捕获异步错误 |
| 自定义错误 | class MyError extends Error |
| 重新抛出 | throw error(让上层处理) |
| 全局捕获 | unhandledrejection 事件 |