Skip to content

JavaScript 错误处理

错误处理是编程中至关重要的部分,它确保程序在遇到异常情况时能够优雅地处理而不是崩溃。JavaScript 提供了多种错误处理机制,包括 try...catch 语句、错误对象、自定义错误等。掌握错误处理技术对于编写健壮、可靠的 JavaScript 应用程序至关重要。在本章节中,我们将深入学习 JavaScript 中的错误处理机制。

什么是错误处理

错误处理是指程序在执行过程中遇到异常情况时,能够捕获、处理并恢复正常执行的机制。良好的错误处理可以:

  1. 防止程序崩溃:避免因未处理的错误导致整个应用程序停止运行
  2. 提供用户友好的错误信息:帮助用户理解发生了什么问题
  3. 便于调试和维护:提供详细的错误信息帮助开发者定位问题
  4. 提高程序健壮性:使程序能够应对各种异常情况

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 错误处理的核心要点:

  1. 错误类型:Error、SyntaxError、ReferenceError、TypeError、RangeError、URIError、EvalError
  2. 基本语法:try...catch...finally 语句
  3. 错误抛出:throw 语句和自定义错误类型
  4. 异步错误处理:Promise.catch()、async/await + try...catch
  5. 全局错误处理:window.onerror、unhandledrejection 事件
  6. 自定义错误:继承 Error 类创建特定业务错误
  7. 最佳实践:早期验证、有意义的错误信息、错误日志记录、优雅恢复、用户友好显示
  8. 实际应用:表单验证、API 错误处理中间件

掌握错误处理是编写高质量 JavaScript 应用程序的关键技能。在下一章节中,我们将学习 JavaScript 的调试技术。

本站内容仅供学习和研究使用。