1. 为什么需要异常处理?
在JavaScript开发中,异常处理就像给代码系上安全带。想象你正在驾驶一辆车,异常处理机制就是车上的安全气囊和ABS系统——它们不会让你的程序在遇到意外时直接"撞毁"。
我见过太多新手开发者写出这样的代码:
javascript复制function getUserData(userId) {
const response = fetch(`/api/users/${userId}`);
const data = response.json();
return data.profile;
}
这段代码至少有3个潜在崩溃点:
- 网络请求可能失败(fetch抛出异常)
- 响应可能不是有效JSON(json()抛出异常)
- data可能没有profile属性(访问undefined属性)
当这些情况发生时,如果没有异常处理,整个应用就会像多米诺骨牌一样崩溃。更糟的是,用户只会看到一个空白页面,完全不知道发生了什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JavaScript异常处理基础
2.1 try-catch-finally三件套
最基本的异常处理结构是这样的:
javascript复制try {
// 可能出错的代码
} catch (error) {
// 出错时执行
} finally {
// 无论是否出错都会执行
}
实际开发中我经常这样用:
javascript复制async function loadUserProfile(userId) {
let loading = true;
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
console.error('加载用户资料失败:', error);
showToast('加载失败,请稍后重试');
throw error; // 继续向上抛出
} finally {
loading = false;
}
}
重要提示:finally块中的代码一定会执行,即使try或catch中有return语句。这在清理资源时特别有用。
2.2 Error对象详解
JavaScript内置了多种Error类型:
- Error:基础错误类型
- SyntaxError:语法错误
- TypeError:类型错误
- ReferenceError:引用错误
- RangeError:数值超出范围
- URIError:URI处理错误
- EvalError:eval函数错误(现在很少见)
自定义错误可以这样:
javascript复制class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = 'ApiError';
}
}
throw new ApiError('无效的用户ID', 400);
在我的项目中,通常会建立完整的错误体系:
javascript复制// errors.js
export class NetworkError extends Error { /*...*/ }
export class AuthError extends Error { /*...*/ }
export class ValidationError extends Error { /*...*/ }
// api.js
import { NetworkError, AuthError } from './errors';
try {
// API调用
} catch (error) {
if (error instanceof AuthError) {
// 跳转到登录页
} else if (error instanceof NetworkError) {
// 显示网络错误提示
}
}
3. 异步代码的异常处理
3.1 Promise的catch陷阱
新手常犯的错误:
javascript复制fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log(data);
});
// 这里漏掉了.catch()
正确的做法应该是:
javascript复制fetch('/api/data')
.then(handleResponse)
.then(processData)
.catch(error => {
console.error('请求失败:', error);
showErrorUI(error);
});
3.2 async/await的最佳实践
使用async/await时,我推荐这种模式:
javascript复制async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('获取数据失败:', error);
throw error; // 或者返回一个错误状态
}
}
}
3.3 并行请求的错误处理
当使用Promise.all时:
javascript复制async function fetchAllData() {
try {
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts()
]);
return { users, products };
} catch (error) {
// 这里会捕获第一个失败的promise
console.error('部分数据加载失败:', error);
// 可以尝试部分回退
const users = await fetchUsers().catch(() => []);
return { users, products: [] };
}
}
对于更精细的控制,可以用Promise.allSettled:
javascript复制const results = await Promise.allSettled([
fetchUsers(),
fetchProducts()
]);
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
4. 高级异常处理技巧
4.1 全局错误捕获
在浏览器端:
javascript复制// 捕获未处理的Promise拒绝
window.addEventListener('unhandledrejection', event => {
console.error('未处理的Promise拒绝:', event.reason);
event.preventDefault(); // 阻止默认错误输出
});
// 捕获全局JS错误
window.addEventListener('error', event => {
console.error('全局错误:', event.error);
sendErrorToServer(event.error);
return true; // 阻止默认错误处理
});
在Node.js中:
javascript复制process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
// 应该记录错误并优雅退出
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的拒绝:', reason);
});
4.2 错误边界(React场景)
在React 16+中可以使用错误边界:
javascript复制class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
// 使用方式
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
4.3 性能监控集成
将错误与性能监控系统集成:
javascript复制// 封装一个错误上报函数
function reportError(error, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
type: error.name,
context,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// 发送到Sentry/LogRocket等
if (window.Sentry) {
Sentry.captureException(error, { extra: context });
}
// 或者发送到自己的后端
navigator.sendBeacon('/api/errors', JSON.stringify(errorData));
}
// 在catch块中使用
try {
// 业务代码
} catch (error) {
reportError(error, { userId: currentUser.id });
throw error;
}
5. 常见陷阱与最佳实践
5.1 不要静默吞掉错误
这是最糟糕的反模式:
javascript复制try {
riskyOperation();
} catch (error) {
// 什么都不做!
}
至少应该记录错误:
javascript复制} catch (error) {
console.error('操作失败:', error);
// 或者显示用户通知
}
5.2 错误信息要有价值
避免这样的错误:
javascript复制throw new Error('出错了!');
应该提供足够上下文:
javascript复制throw new Error(`用户更新失败: 无效的邮箱地址 "${email}"`);
5.3 不要过度使用try-catch
不是所有代码都需要try-catch:
javascript复制// 不必要
try {
const name = 'John';
console.log(name.toUpperCase());
} catch (error) {
// ...
}
// 必要的
try {
const data = JSON.parse(userInput);
} catch (error) {
// JSON.parse可能失败
}
5.4 错误处理与业务逻辑分离
我推荐这种结构:
javascript复制// 业务逻辑
async function updateUserProfile(userId, updates) {
validateInput(updates);
const sanitized = sanitizeInput(updates);
return await api.patch(`/users/${userId}`, sanitized);
}
// 错误处理层
async function safeUpdateProfile(userId, updates) {
try {
return await updateUserProfile(userId, updates);
} catch (error) {
if (error instanceof ValidationError) {
showAlert('输入无效: ' + error.message);
} else {
showAlert('更新失败,请稍后重试');
throw error;
}
}
}
6. 实战:构建健壮的API客户端
让我们实现一个完整的API客户端示例:
javascript复制class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
async request(endpoint, options = {}) {
const { method = 'GET', body, headers = {} } = options;
const url = `${this.baseURL}${endpoint}`;
const config = {
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await this.parseError(response);
throw this.createApiError(response.status, errorData);
}
return await this.parseResponse(response);
} catch (error) {
if (error instanceof ApiError) {
this.handleApiError(error);
} else {
this.handleNetworkError(error);
}
throw error;
}
}
async parseResponse(response) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return response.json();
}
return response.text();
}
async parseError(response) {
try {
return await this.parseResponse(response);
} catch {
return { message: `HTTP错误 ${response.status}` };
}
}
createApiError(status, data) {
const message = data.message || `请求失败,状态码: ${status}`;
return new ApiError(message, status, data);
}
handleApiError(error) {
console.error('API错误:', error);
if (error.status === 401) {
// 跳转到登录
}
// 其他状态码处理...
}
handleNetworkError(error) {
console.error('网络错误:', error);
showToast('网络连接异常,请检查后重试');
}
}
// 使用示例
const api = new ApiClient('https://api.example.com');
async function getProducts() {
try {
return await api.request('/products');
} catch (error) {
// 错误已经被处理过,这里可以选择是否继续抛出
return [];
}
}
这个客户端处理了:
- 网络错误
- HTTP错误状态码
- 响应解析
- 统一的错误格式
- 全局错误处理
7. 错误处理与TypeScript
使用TypeScript可以增强错误处理的安全性:
typescript复制interface ApiError extends Error {
statusCode: number;
data?: unknown;
}
function isApiError(error: unknown): error is ApiError {
return error instanceof Error && 'statusCode' in error;
}
async function fetchData(): Promise<Data> {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`);
}
return await response.json() as Data;
} catch (error) {
if (isApiError(error)) {
console.error(`API错误 ${error.statusCode}:`, error);
} else {
console.error('未知错误:', error);
}
throw error;
}
}
TypeScript的类型保护让我们可以更安全地处理不同类型的错误。
8. 测试中的异常处理
测试错误场景同样重要:
javascript复制describe('API客户端', () => {
it('应该正确处理404错误', async () => {
fetch.mockResponseOnce('', { status: 404 });
try {
await api.request('/not-found');
fail('应该抛出错误');
} catch (error) {
expect(error).toBeInstanceOf(ApiError);
expect(error.statusCode).toBe(404);
}
});
it('应该处理网络错误', async () => {
fetch.mockReject(new Error('网络错误'));
await expect(api.request('/test'))
.rejects
.toThrow('网络连接异常');
});
});
在我的项目中,错误场景的测试用例通常会占到30%以上。
