JavaScript 错误处理

深入解析 JavaScript 错误处理机制:Error 对象的属性、自定义错误类、try/catch/finally 执行顺序,以及异步错误处理和 unhandledrejection。

为什么错误处理是健壮代码的基础

任何程序都会遇到错误——网络中断、用户输入无效、数据格式错误。错误处理的质量直接决定了代码的健壮性和可维护性。

这篇文章深入解析 JavaScript 的错误处理机制,从基础语法到高级模式,帮助你写出能优雅处理错误的代码。


一、Error 对象

1.1 Error 的属性

const error = new Error('Something went wrong');

console.log(error.name);    // 'Error'
console.log(error.message); // 'Something went wrong'
console.log(error.stack);  // 错误堆栈字符串

// 常见 Error 子类
console.log(new TypeError('Expected a string').name); // 'TypeError'
console.log(new RangeError('Value out of range').name); // 'RangeError'
console.log(new SyntaxError('Invalid syntax').name); // 'SyntaxError'
console.log(new ReferenceError('Undefined variable').name); // 'ReferenceError'
console.log(new URIError('URI error').name); // 'URIError'

1.2 Error 堆栈

function foo() {
  bar();
}

function bar() {
  baz();
}

function baz() {
  console.log(new Error().stack);
}

foo();
/*
Error
    at baz (/path/to/file.js:12:17)
    at bar (/path/to/file.js:8:5)
    at foo (/path/to/file.js:4:5)
    at Object.<anonymous> (/path/to/file.js:15:1)
*/

二、自定义错误类

2.1 基本自定义错误

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new ValidationError('Age must be a number', 'age');
  }
  if (age < 0 || age > 150) {
    throw new ValidationError('Age must be between 0 and 150', 'age');
  }
}

try {
  validateAge('invalid');
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Validation failed for ${e.field}: ${e.message}`);
  }
}

2.2 错误类型体系

class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
  }
}

class ValidationError extends AppError {
  constructor(message, field) {
    super(message, 400);
    this.field = field;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
    this.resource = resource;
  }
}

class UnauthorizedError extends AppError {
  constructor() {
    super('Unauthorized', 401);
  }
}

三、try / catch / finally

3.1 执行顺序

try {
  console.log('1. try block start');
  // throw new Error('test');
  console.log('2. try block end'); // 如果抛出异常,这行不执行
} catch (e) {
  console.log('3. catch block:', e.message);
} finally {
  console.log('4. finally block'); // 始终执行
}

// 正常执行输出顺序:1 → 2 → 4
// 抛出异常输出顺序:1 → 3 → 4

3.2 catch 的块级作用域

// var 的问题
try {
  throw new Error('test');
} catch (e) {
  var errorMessage = e.message; // var 是函数作用域,不是块级作用域
}

console.log(errorMessage); // 'test' — 泄漏到外部

// const/let 是真正的块级作用域
try {
  throw new Error('test');
} catch (e) {
  const errorMessage = e.message; // 真正被捕获
}
// console.log(errorMessage); // ReferenceError

3.3 finally 与 return

function foo() {
  try {
    return 'try';
  } finally {
    console.log('finally');
  }
}
// 输出:finally,然后返回 'try'

function bar() {
  try {
    throw new Error('test');
  } catch (e) {
    return 'catch';
  } finally {
    console.log('finally');
  }
}
// 输出:finally,然后返回 'catch'

四、异步错误处理

4.1 Promise 的 .catch

fetch('/api/data')
  .then(response => {
    if (!response.ok) throw new Error('Network response was not ok');
    return response.json();
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

4.2 async/await 中的 try/catch

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error('Network response was not ok');
    return await response.json();
  } catch (error) {
    console.error('Fetch error:', error.message);
    return null;
  }
}

// 多个错误分别处理
async function handleRequests() {
  const [userResult, postsResult] = await Promise.allSettled([
    fetch('/api/user'),
    fetch('/api/posts')
  ]);

  if (userResult.status === 'rejected') {
    console.error('User fetch failed:', userResult.reason);
  }

  if (postsResult.status === 'rejected') {
    console.error('Posts fetch failed:', postsResult.reason);
  }
}

4.3 全局 unhandledrejection

// 未处理的 Promise 拒绝会被触发
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
});

// 防止未处理的拒绝
window.addEventListener('rejectionhandled', (event) => {
  console.log('Promise rejection was handled:', event.reason);
});

五、错误处理最佳实践

5.1 不要捕获所有错误

// 错误做法:捕获所有,隐藏问题
try {
  // ...
} catch (e) {
  console.log('Error'); // 丢失错误信息
}

// 正确做法:只捕获能处理的错误
try {
  JSON.parse(userInput);
} catch (e) {
  if (e instanceof SyntaxError) {
    return { error: 'Invalid JSON format' };
  }
  throw e; // 其他错误重新抛出
}

5.2 使用 finally 清理资源

function readFile(path) {
  let file = null;
  try {
    file = openFile(path);
    return file.read();
  } finally {
    if (file) {
      file.close(); // 始终关闭文件
    }
  }
}

5.3 错误边界(Error Boundaries)

// React 错误边界示例
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

六、延展阅读