JavaScript 异常处理

简介

异常(Exception)是程序运行时发生的错误。JavaScript 提供了 try...catch...finally 语句和 throw 语句来处理异常,防止程序因错误而崩溃。


try…catch 语句

基本语法

try {
  // 尝试执行的代码
} catch (error) {
  // 发生错误时执行的代码
}

执行流程

  1. 执行 try 块中的代码
  2. 如果没有错误,跳过 catch
  3. 如果发生错误,停止执行 try 块中的剩余代码,跳转到 catch
  4. 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 事件