JavaScript 错误处理
错误处理是编程中至关重要的部分,它确保程序在遇到异常情况时能够优雅地处理而不是崩溃。JavaScript 提供了多种错误处理机制,包括 try...catch 语句、错误对象、自定义错误等。掌握错误处理技术对于编写健壮、可靠的 JavaScript 应用程序至关重要。在本章节中,我们将深入学习 JavaScript 中的错误处理机制。
什么是错误处理
错误处理是指程序在执行过程中遇到异常情况时,能够捕获、处理并恢复正常执行的机制。良好的错误处理可以:
- 防止程序崩溃:避免因未处理的错误导致整个应用程序停止运行
- 提供用户友好的错误信息:帮助用户理解发生了什么问题
- 便于调试和维护:提供详细的错误信息帮助开发者定位问题
- 提高程序健壮性:使程序能够应对各种异常情况
JavaScript 中的错误类型
JavaScript 中有多种内置的错误类型:
1. Error(基本错误类型)
javascript
const basicError = new Error("这是一个基本错误");
console.log(basicError.name); // "Error"
console.log(basicError.message); // "这是一个基本错误"
console.log(basicError.stack); // 错误堆栈信息2. SyntaxError(语法错误)
javascript
try {
eval("const a = ;"); // 语法错误
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // "Unexpected token ';'"
}3. ReferenceError(引用错误)
javascript
try {
console.log(undefinedVariable); // 引用未声明的变量
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedVariable is not defined"
}4. TypeError(类型错误)
javascript
try {
const num = 123;
num.toUpperCase(); // 数字没有 toUpperCase 方法
} catch (error) {
console.log(error.name); // "TypeError"
console.log(error.message); // "num.toUpperCase is not a function"
}5. RangeError(范围错误)
javascript
try {
const arr = new Array(-1); // 数组长度不能为负数
} catch (error) {
console.log(error.name); // "RangeError"
console.log(error.message); // "Invalid array length"
}6. URIError(URI 错误)
javascript
try {
decodeURIComponent("%"); // 无效的 URI 组件
} catch (error) {
console.log(error.name); // "URIError"
console.log(error.message); // "URI malformed"
}7. EvalError(eval 错误)
javascript
// EvalError 在现代 JavaScript 中很少使用
try {
throw new EvalError("Eval 错误");
} catch (error) {
console.log(error.name); // "EvalError"
console.log(error.message); // "Eval 错误"
}try...catch 语句
try...catch 语句是 JavaScript 中最基本的错误处理机制。
基本语法
javascript
try {
// 可能出现错误的代码
console.log("尝试执行的代码");
} catch (error) {
// 处理错误的代码
console.log("捕获到错误:" + error.message);
}完整的 try...catch...finally 结构
javascript
try {
console.log("尝试执行的代码");
throw new Error("手动抛出错误");
} catch (error) {
console.log("捕获到错误:" + error.message);
} finally {
console.log("无论是否有错误都会执行的代码");
}错误对象的属性
javascript
try {
throw new Error("测试错误");
} catch (error) {
console.log("错误名称:" + error.name); // "Error"
console.log("错误消息:" + error.message); // "测试错误"
console.log("错误堆栈:" + error.stack); // 错误发生的位置信息
console.log("错误实例:" + error instanceof Error); // true
}throw 语句
throw 语句用于手动抛出错误。
抛出内置错误
javascript
function validateAge(age) {
if (age < 0) {
throw new RangeError("年龄不能为负数");
}
if (age > 150) {
throw new RangeError("年龄不能超过150岁");
}
return true;
}
try {
validateAge(-5);
} catch (error) {
console.log(error.name + ": " + error.message);
}抛出自定义错误
javascript
function divide(a, b) {
if (b === 0) {
throw new Error("除数不能为零");
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.log("计算错误:" + error.message);
}抛出其他类型的值
javascript
try {
throw "字符串错误";
} catch (error) {
console.log("捕获到:" + error); // "字符串错误"
}
try {
throw 42;
} catch (error) {
console.log("捕获到数字:" + error); // 42
}
try {
throw { code: 500, message: "服务器错误" };
} catch (error) {
console.log("捕获到对象:" + error.message); // "服务器错误"
}自定义错误类型
创建自定义错误类型可以提供更具体的错误信息。
继承 Error 类
javascript
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
// 使用自定义错误
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("邮箱格式不正确");
}
return true;
}
function fetchData(url) {
// 模拟网络请求失败
throw new NetworkError("网络请求失败", 500);
}
// 处理不同类型的错误
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log("验证错误:" + error.message);
} else if (error instanceof NetworkError) {
console.log("网络错误:" + error.message + " (状态码: " + error.statusCode + ")");
} else {
console.log("未知错误:" + error.message);
}
}带有额外属性的自定义错误
javascript
class ApiError extends Error {
constructor(message, code, details = null) {
super(message);
this.name = "ApiError";
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
// 使用带有额外属性的自定义错误
try {
throw new ApiError("API 调用失败", "API_001", {
endpoint: "/api/users",
method: "GET",
userId: 123
});
} catch (error) {
if (error instanceof ApiError) {
console.log("API 错误:");
console.log("消息:" + error.message);
console.log("代码:" + error.code);
console.log("详情:" + JSON.stringify(error.details));
console.log("时间:" + error.timestamp);
}
}异步错误处理
异步操作中的错误处理需要特殊注意。
Promise 错误处理
javascript
// 使用 .catch() 处理 Promise 错误
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("数据获取失败"));
}, 1000);
});
}
fetchData()
.then(data => {
console.log("数据:" + data);
})
.catch(error => {
console.log("Promise 错误:" + error.message);
});
// 链式 Promise 错误处理
function processUserData() {
return fetchData()
.then(data => {
// 处理数据
return processData(data);
})
.then(processedData => {
// 进一步处理
return saveData(processedData);
})
.catch(error => {
console.log("处理链中的错误:" + error.message);
// 可以返回默认值或重新抛出错误
return { error: error.message, data: null };
});
}async/await 错误处理
javascript
// 使用 try...catch 处理 async/await 错误
async function handleUserData() {
try {
const data = await fetchData();
const processedData = await processData(data);
const result = await saveData(processedData);
return result;
} catch (error) {
console.log("异步操作错误:" + error.message);
// 可以重新抛出错误或返回默认值
throw new Error("用户数据处理失败:" + error.message);
}
}
// 调用异步函数
handleUserData()
.then(result => {
console.log("处理成功:" + result);
})
.catch(error => {
console.log("最终错误:" + error.message);
});并行异步操作的错误处理
javascript
// Promise.all 错误处理
async function fetchMultipleData() {
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
} catch (error) {
console.log("并行操作中有错误:" + error.message);
throw error;
}
}
// Promise.allSettled 处理(ES2020)
async function fetchMultipleDataGracefully() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
const successful = results
.filter(result => result.status === "fulfilled")
.map(result => result.value);
const errors = results
.filter(result => result.status === "rejected")
.map(result => result.reason);
console.log("成功的结果:" + successful.length);
console.log("错误的数量:" + errors.length);
errors.forEach(error => {
console.log("错误详情:" + error.message);
});
return { successful, errors };
}全局错误处理
window.onerror(浏览器环境)
javascript
// 全局 JavaScript 错误处理
window.onerror = function(message, source, lineno, colno, error) {
console.log("全局错误捕获:");
console.log("消息:" + message);
console.log("源文件:" + source);
console.log("行号:" + lineno);
console.log("列号:" + colno);
console.log("错误对象:" + error);
// 可以发送错误信息到服务器进行记录
// sendErrorToServer({ message, source, lineno, colno, error });
// 返回 true 阻止默认的错误处理
return true;
};
// 测试全局错误处理
// throw new Error("测试全局错误处理");unhandledrejection(Promise 未处理拒绝)
javascript
// 处理未捕获的 Promise 拒绝
window.addEventListener("unhandledrejection", function(event) {
console.log("未处理的 Promise 拒绝:");
console.log("原因:" + event.reason);
console.log("Promise:" + event.promise);
// 阻止默认的处理(在控制台显示错误)
event.preventDefault();
});
// 测试未处理的 Promise 拒绝
// Promise.reject(new Error("未处理的 Promise 错误"));Node.js 环境的全局错误处理
javascript
// Node.js 中的全局错误处理
process.on("uncaughtException", (error) => {
console.log("未捕获的异常:" + error.message);
console.log(error.stack);
// 记录错误并优雅退出
// logError(error);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.log("未处理的 Promise 拒绝:" + reason);
console.log("Promise:" + promise);
// 可以选择退出进程或继续运行
// process.exit(1);
});错误处理的最佳实践
1. 早期验证和错误检查
javascript
function calculateBMI(weight, height) {
// 参数验证
if (typeof weight !== "number" || weight <= 0) {
throw new ValidationError("体重必须是正数");
}
if (typeof height !== "number" || height <= 0) {
throw new ValidationError("身高必须是正数");
}
if (height > 3) {
throw new ValidationError("身高单位应该是米");
}
// 计算 BMI
const bmi = weight / (height * height);
return Math.round(bmi * 100) / 100;
}
// 使用示例
try {
const bmi = calculateBMI(70, 1.75);
console.log("BMI:" + bmi);
} catch (error) {
console.log("计算错误:" + error.message);
}2. 提供有意义的错误信息
javascript
class UserService {
static users = [];
static addUser(userData) {
// 验证必要字段
if (!userData.name) {
throw new ValidationError("用户姓名是必需的");
}
if (!userData.email) {
throw new ValidationError("用户邮箱是必需的");
}
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new ValidationError("邮箱格式不正确");
}
// 检查邮箱是否已存在
if (this.users.some(user => user.email === userData.email)) {
throw new ValidationError("该邮箱已被使用");
}
// 创建用户
const user = {
id: Date.now(),
...userData,
createdAt: new Date()
};
this.users.push(user);
return user;
}
static getUserById(id) {
const user = this.users.find(user => user.id === id);
if (!user) {
throw new Error(`未找到 ID 为 ${id} 的用户`);
}
return user;
}
}3. 错误日志记录
javascript
class ErrorLogger {
static log(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
name: error.name,
stack: error.stack,
context: context,
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "Node.js",
url: typeof window !== "undefined" ? window.location.href : "Node.js"
};
// 在开发环境中输出到控制台
if (process.env.NODE_ENV === "development") {
console.error("错误日志:", errorInfo);
}
// 发送到服务器进行记录
// this.sendToServer(errorInfo);
return errorInfo;
}
static sendToServer(errorInfo) {
// 模拟发送到服务器
fetch("/api/errors", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(errorInfo)
}).catch(sendError => {
console.error("发送错误日志失败:", sendError);
});
}
}
// 使用错误日志记录
try {
// 可能出错的代码
throw new Error("测试错误");
} catch (error) {
ErrorLogger.log(error, {
component: "UserComponent",
action: "saveUser",
userId: 123
});
}4. 优雅的错误恢复
javascript
class DataFetcher {
static async fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.log(`第 ${i + 1} 次尝试失败:${error.message}`);
// 如果不是最后一次尝试,等待一段时间后重试
if (i < maxRetries - 1) {
await this.delay(1000 * (i + 1)); // 递增延迟
}
}
}
// 所有重试都失败了
throw new Error(`经过 ${maxRetries} 次重试后仍然失败:${lastError.message}`);
}
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用带重试的获取函数
DataFetcher.fetchWithRetry("https://api.example.com/data")
.then(data => {
console.log("数据获取成功:", data);
})
.catch(error => {
console.log("最终失败:", error.message);
});5. 用户友好的错误显示
javascript
class ErrorHandler {
static displayUserFriendlyError(error) {
let userMessage = "发生未知错误,请稍后重试";
if (error instanceof ValidationError) {
userMessage = error.message;
} else if (error instanceof NetworkError) {
userMessage = "网络连接失败,请检查网络设置";
} else if (error.message.includes("timeout")) {
userMessage = "请求超时,请稍后重试";
} else if (error.message.includes("404")) {
userMessage = "请求的资源未找到";
} else if (error.message.includes("500")) {
userMessage = "服务器内部错误,请稍后重试";
}
// 显示错误消息给用户
this.showMessage(userMessage, "error");
// 记录详细错误信息
console.error("详细错误信息:", error);
}
static showMessage(message, type = "info") {
// 创建错误消息元素
const messageElement = document.createElement("div");
messageElement.className = `message message-${type}`;
messageElement.textContent = message;
// 添加到页面
document.body.appendChild(messageElement);
// 3秒后自动移除
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.parentNode.removeChild(messageElement);
}
}, 3000);
}
}
// 使用用户友好的错误处理
async function saveUser(userData) {
try {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
ErrorHandler.displayUserFriendlyError(error);
throw error;
}
}实际应用示例
1. 表单验证和错误处理
javascript
class FormValidator {
constructor(formElement) {
this.form = formElement;
this.errors = new Map();
this.init();
}
init() {
this.form.addEventListener("submit", (event) => {
event.preventDefault();
this.validateAndSubmit();
});
// 实时验证
this.form.querySelectorAll("input, select, textarea").forEach(field => {
field.addEventListener("blur", () => {
this.validateField(field);
});
});
}
async validateAndSubmit() {
try {
// 清除之前的错误
this.clearErrors();
// 验证所有字段
const isValid = this.validateAllFields();
if (!isValid) {
throw new ValidationError("请修正表单中的错误");
}
// 获取表单数据
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
// 提交数据
this.showLoading(true);
const result = await this.submitData(data);
this.showSuccess("提交成功!");
return result;
} catch (error) {
this.handleError(error);
} finally {
this.showLoading(false);
}
}
validateField(field) {
const fieldName = field.name;
const value = field.value.trim();
let errorMessage = "";
// 根据字段类型进行验证
switch (fieldName) {
case "email":
if (!value) {
errorMessage = "邮箱是必需的";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errorMessage = "请输入有效的邮箱地址";
}
break;
case "password":
if (!value) {
errorMessage = "密码是必需的";
} else if (value.length < 8) {
errorMessage = "密码至少需要8个字符";
}
break;
case "age":
if (value && (isNaN(value) || value < 0 || value > 150)) {
errorMessage = "请输入有效的年龄";
}
break;
default:
if (field.hasAttribute("required") && !value) {
errorMessage = "此字段是必需的";
}
}
this.setFieldError(field, errorMessage);
return !errorMessage;
}
validateAllFields() {
let isValid = true;
this.form.querySelectorAll("input, select, textarea").forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
setFieldError(field, errorMessage) {
const fieldName = field.name;
// 更新错误状态
if (errorMessage) {
this.errors.set(fieldName, errorMessage);
field.classList.add("error");
} else {
this.errors.delete(fieldName);
field.classList.remove("error");
}
// 显示或隐藏错误消息
this.updateFieldErrorMessage(field, errorMessage);
}
updateFieldErrorMessage(field, errorMessage) {
const errorElement = field.parentNode.querySelector(".error-message");
if (errorElement) {
errorElement.textContent = errorMessage;
}
}
async submitData(data) {
const response = await fetch("/api/submit", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `提交失败:${response.status}`);
}
return await response.json();
}
handleError(error) {
if (error instanceof ValidationError) {
this.showMessage(error.message, "error");
} else {
this.showMessage("提交失败,请稍后重试", "error");
console.error("表单提交错误:", error);
}
// 滚动到第一个错误字段
const firstErrorField = this.form.querySelector(".error");
if (firstErrorField) {
firstErrorField.scrollIntoView({ behavior: "smooth", block: "center" });
firstErrorField.focus();
}
}
clearErrors() {
this.errors.clear();
this.form.querySelectorAll(".error").forEach(field => {
field.classList.remove("error");
});
this.form.querySelectorAll(".error-message").forEach(element => {
element.textContent = "";
});
}
showLoading(show) {
const submitButton = this.form.querySelector("button[type='submit']");
if (submitButton) {
submitButton.disabled = show;
submitButton.textContent = show ? "提交中..." : "提交";
}
}
showSuccess(message) {
this.showMessage(message, "success");
}
showMessage(message, type) {
// 移除之前的消息
const existingMessage = this.form.querySelector(".form-message");
if (existingMessage) {
existingMessage.remove();
}
// 创建新消息
const messageElement = document.createElement("div");
messageElement.className = `form-message message-${type}`;
messageElement.textContent = message;
// 添加到表单顶部
this.form.insertBefore(messageElement, this.form.firstChild);
// 3秒后自动移除
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.remove();
}
}, 3000);
}
}
// 使用示例
// const validator = new FormValidator(document.getElementById("myForm"));2. API 错误处理中间件
javascript
class ApiErrorHandler {
static handle(error, request, response, next) {
// 记录错误
console.error("API 错误:", {
timestamp: new Date().toISOString(),
error: error.message,
stack: error.stack,
url: request.url,
method: request.method,
userAgent: request.get("User-Agent")
});
// 根据错误类型返回不同的响应
if (error instanceof ValidationError) {
return response.status(400).json({
error: "验证错误",
message: error.message,
details: error.details || null
});
}
if (error instanceof AuthenticationError) {
return response.status(401).json({
error: "认证错误",
message: error.message
});
}
if (error instanceof AuthorizationError) {
return response.status(403).json({
error: "授权错误",
message: error.message
});
}
if (error instanceof NotFoundError) {
return response.status(404).json({
error: "资源未找到",
message: error.message
});
}
// 数据库错误
if (error.name === "SequelizeUniqueConstraintError") {
return response.status(409).json({
error: "冲突错误",
message: "数据已存在"
});
}
if (error.name === "SequelizeForeignKeyConstraintError") {
return response.status(409).json({
error: "关联错误",
message: "存在关联数据,无法删除"
});
}
// 默认服务器错误
return response.status(500).json({
error: "服务器内部错误",
message: "请稍后重试"
});
}
}
// 自定义错误类
class ValidationError extends Error {
constructor(message, details = null) {
super(message);
this.name = "ValidationError";
this.details = details;
}
}
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = "AuthenticationError";
}
}
class AuthorizationError extends Error {
constructor(message) {
super(message);
this.name = "AuthorizationError";
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
}总结
JavaScript 错误处理的核心要点:
- 错误类型:Error、SyntaxError、ReferenceError、TypeError、RangeError、URIError、EvalError
- 基本语法:try...catch...finally 语句
- 错误抛出:throw 语句和自定义错误类型
- 异步错误处理:Promise.catch()、async/await + try...catch
- 全局错误处理:window.onerror、unhandledrejection 事件
- 自定义错误:继承 Error 类创建特定业务错误
- 最佳实践:早期验证、有意义的错误信息、错误日志记录、优雅恢复、用户友好显示
- 实际应用:表单验证、API 错误处理中间件
掌握错误处理是编写高质量 JavaScript 应用程序的关键技能。在下一章节中,我们将学习 JavaScript 的调试技术。