FORMA

错误处理与调试

结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。

一、错误构造函数

JavaScript 内置了多个错误构造函数,所有内置错误都继承自 Error 类。

1. Error

最通用的错误类型,用于创建自定义错误。接受可选的 message 参数。

js
const err = new Error("Something went wrong");
console.log(err.name); // 'Error'
console.log(err.message); // 'Something went wrong'
console.log(err.stack); // 堆栈信息

2. 特定错误类型

构造函数触发场景
SyntaxError代码语法错误(如缺少括号、非法变量名)
TypeError值类型与预期不符(如调用非函数、读取 null 的属性)
RangeError数值超出有效范围(如数组长度负数、递归栈溢出)
ReferenceError引用未声明的变量(在严格模式下更常见)
URIError全局 URI 处理函数(decodeURI 等)传入了无效参数
EvalError已废弃,不再使用

示例

js
// SyntaxError(解析阶段,如缺少括号)
// function foo( {

// TypeError
const n = null;
n.toString();

// RangeError
new Array(-1);

// ReferenceError
console.log(undeclaredVar);

可以用 instanceof 判断错误类型:

js
try {
    // 可能抛出 TypeError 的代码
} catch (err) {
    if (err instanceof TypeError) {
        console.error("类型错误");
    } else if (err instanceof RangeError) {
        console.error("范围错误");
    } else {
        console.error("其他错误");
    }
}

二、自定义错误类(继承 Error)

通过继承 Error 可以创建自定义错误类型,便于区分业务逻辑错误和系统错误。

最佳实践

  • 自定义子类必须调用 super(message)
  • 需要手动设置 name 属性和维护原型链(ES6 的 extends 会自动修复原型,但设置 name 仍可取)。
  • 可选地设置 codestatus 等自定义属性。
js
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "ValidationError";
        this.field = field; // 自定义属性
        // 维护堆栈(可选,ES6 后 prototype 已自动关联)
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, ValidationError);
        }
    }
}

try {
    throw new ValidationError("Invalid email", "email");
} catch (err) {
    console.log(err.name); // 'ValidationError'
    console.log(err.message); // 'Invalid email'
    console.log(err.field); // 'email'
    console.log(err.stack); // 堆栈信息
}

注意:在 TypeScript 中,自定义错误类可能会丢失 instanceof 行为,建议同时设置原型。

三、全局错误捕获

1. 浏览器全局捕获

window.onerror

捕获大多数同步错误和运行时错误(不能捕获 unhandledrejection 和资源加载错误)。

js
window.onerror = function (message, source, lineno, colno, error) {
    console.log("捕获到错误:", message, source, lineno, colno, error);
    // 返回 true 可以阻止默认控制台输出
    return true;
};

window.addEventListener('error')

可以捕获资源加载错误(如 <img><script> 加载失败)。

js
window.addEventListener(
    "error",
    event => {
        if (event.target.tagName) {
            // 资源加载错误
            console.log("资源加载失败:", event.target.src);
        } else {
            // JS 运行时错误
            console.log("JS 错误:", event.message);
        }
    },
    true
); // 使用捕获阶段

unhandledrejection

捕获未处理的 Promise rejection。

js
window.addEventListener("unhandledrejection", event => {
    console.log("未处理的 Promise 拒绝:", event.reason);
    event.preventDefault(); // 可选:阻止控制台默认输出
});

2. Node.js 全局捕获

process.on('uncaughtException')

捕获未捕获的同步异常(不推荐作为生产环境的常规恢复手段,因为程序状态可能不一致)。

js
process.on("uncaughtException", err => {
    console.error("未捕获异常:", err);
    // 最佳实践:记录日志后优雅退出
    process.exit(1);
});

process.on('unhandledRejection')

捕获未处理的 Promise rejection。

js
process.on("unhandledRejection", (reason, promise) => {
    console.error("未处理的拒绝:", reason);
});

四、try/catch/finallythrow

1. try/catch/finally

js
try {
    // 可能抛出错误的代码
    riskyOperation();
} catch (err) {
    // 错误处理
    console.error(err.message);
} finally {
    // 无论是否发生错误都会执行(资源清理、关闭连接等)
    cleanup();
}
  • finally 中的 return 会覆盖 catch 中的 return
  • 如果 try 中有 returnfinally 依然会在返回之前执行。

2. throw

可以抛出任何值(字符串、数字、对象等),但最佳实践是抛出 Error 实例或其子类。

js
function divide(a, b) {
    if (b === 0) {
        throw new Error("Division by zero");
    }
    return a / b;
}

注意:在 catch 块中可以重新抛出错误,以便上层处理。

js
try {
    // ...
} catch (err) {
    if (err instanceof ValidationError) {
        // 处理已知错误
    } else {
        throw err; // 向上传递未知错误
    }
}

五、栈追踪与断点调试

1. 栈追踪(Stack Trace)

错误对象的 stack 属性提供了调用栈信息,便于定位问题。

手动获取栈追踪

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

在 Node.js 中获取更详细的栈信息(使用 --stack-trace-limitError.captureStackTrace

js
const err = new Error("custom");
Error.captureStackTrace(err, myFunction); // 排除 myFunction 自身

2. 浏览器 DevTools 调试

  • 断点:在 Sources 面板点击行号设置,代码执行到此处暂停。
  • 条件断点:右键行号,设置条件表达式。
  • 日志点:打印日志而不暂停。
  • 调用栈面板:查看函数调用链。
  • Scope 面板:查看当前作用域内的变量。
  • Watch 表达式:监控特定变量变化。
  • 步进控制:Step over (F10)、Step into (F11)、Step out (Shift+F11)、Resume (F8)。

常用技巧

  • debugger 语句:在代码中手动插入 debugger;,当 DevTools 打开时自动断点。
js
function complexCalc(a, b) {
    debugger; // 执行到这里会暂停
    return a + b;
}
  • 异步堆栈:Chrome 支持异步调用链追踪,可以查看 setTimeout、Promise、async/await 的完整调用路径。

3. Node.js 调试

命令行调试(--inspect

bash
node --inspect-brk app.js

然后在 Chrome 浏览器打开 chrome://inspect,即可使用 DevTools 进行调试。

VS Code 调试

配置 .vscode/launch.json 文件,支持断点、变量查看、调用栈等。

打印调试与 util.inspect

js
const util = require("util");
console.log(
    util.inspect(obj, { showHidden: false, depth: null, colors: true })
);

Node.js 内置调试器(node inspect

bash
node inspect app.js

使用 contnextwatch 等命令,但体验不如 Chrome DevTools。

4. 高级:长栈追踪(Long Stack Traces)

对于异步错误,传统栈可能仅显示事件循环的当前片段。可以使用第三方库(如 longjohn)或 Promise 库的 done() 方法来增强,但现代 Node.js 的 async_hooks 和 Chrome 的异步栈已经部分解决此问题。

总结

方面核心要点
错误构造函数Error 及子类型 SyntaxErrorTypeErrorRangeErrorReferenceError
自定义错误继承 Error,设置 name 属性,可添加额外字段
全局捕获(浏览器)window.onerror(同步错误)、unhandledrejection(Promise 拒绝)
全局捕获(Node.js)uncaughtException(谨慎使用)、unhandledRejection
try/catch/finally结构化异常处理,finally 用于清理,throw 抛出任意值(建议 Error 实例)
调试栈追踪 + 浏览器 DevTools(断点、条件断点、debugger)+ Node.js --inspect

生产环境应记录日志并监控未捕获异常;具体工具选型因项目而异。

参考文献

以下链接在编写时均可正常访问:

资料说明
MDN:Error错误对象
MDN:try...catch异常处理
MDN:unhandledrejectionPromise 未处理拒绝
Node.js:process 事件Node 全局错误(英文)