← Node.js 定时器 DNS 模块 →

Node.js错误处理全解析:Error对象、系统错误码与自定义异常教程

著
原创 2026-10-12 Node.js 已有人查阅

为什么错误处理决定了Node.js应用的健壮性

Node.js运行在单线程事件循环之上,一个未捕获的异常可能导致整个进程退出。与浏览器环境不同,服务端的错误往往直接影响接口可用性和数据一致性。理解错误的分类、传播路径与拦截时机,是编写可维护Node.js服务的基础能力。

在代码号学习编程的过程中,很多开发者习惯性地用console.log打印错误就认为处理完毕。实际项目中,错误需要被分类、转换、记录,并最终以合适的HTTP状态码或业务码返回给调用方。下面从错误类型开始逐层展开。

Node.js错误的本质与Error对象

Node.js中的错误是JavaScript内置Error对象的实例。当异常条件触发时,运行时环境会生成一个错误对象,并附带调用栈信息。调用栈记录了错误发生的具置以及导致错误的函数调用序列,这对定位问题非常重要。

一个标准的Error实例包含以下核心属性:

属性 作用
name 错误类型标识,如TypeError、ReferenceError,用于高层级分类过滤
message 创建错误时传入的描述信息,应包含可读的诊断线索,但不得泄露数据库密码、内部令牌等敏感数据
stack 错误名称、消息以及完整的函数调用轨迹,是排查问题的第一手资料
code Node.js特有的扩展属性,用于标识具体的系统级错误类型,如ENOENT、EACCES

在项目中,name和code的配合使用尤为关键。例如在日志系统中,可以按name做告警分级,按code做重试策略判断。需要留意的是,stack属性在生产环境不应直接返回给客户端,它可能暴露文件路径和内部模块结构。

Node.js错误的四大类型

JavaScript原生错误

这类错误由JavaScript引擎直接抛出,属于运行时错误。常见的包括:

  • TypeError:对错误类型的值执行了不合法操作

  • ReferenceError:引用了未声明的变量

  • SyntaxError:代码语法不合法

  • RangeError:数值超出有效范围

代码号示例:

// 代码号学习编程:JavaScript 原生错误演示
const base = 12;
const result = base + unknownVar;
console.log(result);

运行后抛出ReferenceError:unknownVarisnotdefined。变量base已声明,但unknownVar从未声明,引擎在解析表达式时直接抛出引用错误。这类错误通常属于编码阶段的问题,应在开发期通过静态检查工具提前发现。

系统错误

系统错误由操作系统或运行时环境触发,常见场景包括文件缺失、网络中断、权限不足、资源耗尽等。Node.js会为这类错误附加code属性。

// 代码号学习编程:系统错误演示
const fs = require("fs");
fs.readFileSync("config.json");

若config.json不存在,控制台输出Error:ENOENT:nosuchfileordirectory。ENOENT即ErrorNOENTry,表示目标文件或目录未找到。

个人经验:在读取配置文件时,不建议直接使用readFileSync裸调用。更稳妥的做法是先判断文件是否存在,或捕获错误后回退到默认配置。直接让系统错误向上冒泡,可能导致服务启动阶段直接崩溃。

用户自定义错误

开发者通过继承Error类来创建业务语义明确的错误类型。这类错误让调用方能够区分“用户不存在”与“数据库连接失败”等不同场景。

// 代码号学习编程:自定义错误类
class ValidationError extends Error {
    constructor(msg) {
        super(msg);
        this.name = "ValidationError";
    }
}
const err = new ValidationError("Invalid user input");
console.log(err.name);
console.log(err.message);

输出ValidationError和Invaliduserinput。自定义错误的价值在于:catch块中可以通过errinstanceofValidationError精确判断错误来源,从而决定返回400还是500。

断言错误

assert模块用于校验条件是否满足预期。断言失败时抛出AssertionError,程序不会继续执行后续语句。

// 代码号学习编程:断言错误演示
const assert = require("node:assert");
const age = 15;
assert.strictEqual(age, 20);
console.log("Age is valid");

由于15!==20,断言失败,console.log不会执行。断言适合用在开发阶段的内部校验,不建议用于处理用户输入等可预期的运行时错误。

四种错误处理方式的适用场景

try-catch:同步代码的优选

try-catch只能捕获同步代码块中抛出的错误。嵌套try-catch时,内层捕获后外层不再处理。

// 代码号学习编程:try-catch 嵌套演示
function trySomething() {
    throw new Error("An error is thrown from trySomething");
}

function init() {
    try {
        try {
            trySomething();
        } catch (err) {
            console.log("Inner catch:", err.message);
        }
    } catch (err) {
        console.log("Outer catch:", err.message);
    }
    console.log("Execution continues after handling the error");
}

init();

内层catch捕获后,外层catch不会触发,程序继续执行。这种嵌套结构在项目中并不常见,更推荐将错误处理集中到统一的中间件或错误边界。

回调函数:异步错误的第一参数约定

Node.js遵循错误优先回调约定:回调函数的第一个参数是错误对象,第二个参数才是结果。

// 代码号学习编程:回调处理异步错误
const fs = require("fs");

fs.readFile("foo.txt", function (err, data) {
    if (err) {
        console.error(err);
    } else {
        console.log(data.toString());
    }
});

console.log("Program terminated");

输出顺序为Programterminated先打印,随后是文件读取错误。这说明异步操作不会阻塞后续代码。回调方式在旧项目中仍然广泛存在,但多层嵌套容易形成回调地狱,现在项目更倾向于Promise和async-await。

async-await:异步代码的同步写法

async-await让异步代码的书写和阅读更接近同步风格。await会暂停当前async函数的执行,直到Promise敲定。

// 代码号学习编程:async-await 错误捕获
async function fun() {
    try {
        const response = await fetch("https://invalid.example");
        console.log(response);
    } catch (err) {
        console.error("Error:", err);
    }
}
fun();

fetch请求失败时,Promise被拒绝,await表达式抛出异常,被catch块捕获。这里有一个常见误区:fetch只有在网络层面失败时才reject,HTTP404或500并不会触发reject,需要手动检查response.ok。

Promise:链式错误传递

Promise的.catch()可以捕获链式调用中任意环节抛出的错误。

// 代码号学习编程:Promise 错误处理
Promise.reject(new Error("Something went wrong"))
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error("Error:", error.message);
    });

由于Promise被拒绝,.then被跳过,.catch接管错误。Promise与回调的核心区别在于:Promise是对象,可以链式组合,错误会沿着链条向后传递,直到遇到最近的catch。

常见Node.js错误码速查

错误码 含义 典型场景
ENOENT 文件或目录不存在 读取不存在的配置文件
EACCES 权限拒绝 脚本无权访问目标资源
EEXIST 文件已存在 创建文件时与已有文件冲突
EISDIR 目标是一个目录 期望文件却传入了文件夹路径
EMFILE 打开文件过多 操作系统文件描述符达到上限

在代码号学习编程的实践中,建议将错误码映射为业务错误码。例如ENOENT映射为“配置缺失”,EACCES映射为“权限异常”,这样日志和监控系统可以按业务维度聚合。

自定义错误类的工程化设计

在构建API时,错误处理的核心目标是:让每一种错误都能对应到明确的HTTP状态码和业务响应。

// 代码号学习编程:自定义错误类
class UserNotFoundError extends Error {
    constructor(message) {
        super(message);
        this.name = "UserNotFoundError";
    }
}

async function findUser(id) {
    try {
        const user = null;
        if (!user) {
            throw new UserNotFoundError("User not found");
        }
        return user;
    } catch (err) {
        console.error(`${err.name}: ${err.message}`);
    }
}

findUser(101);

输出UserNotFoundError:Usernotfound。这个模式的价值在于:上层调用方可以通过err.name或instanceof判断错误类型,进而决定返回404还是500。

个人建议:自定义错误类应包含三个要素——语义化的类名、明确的name属性、可选的statusCode字段。例如:

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

这样在Express或Koa的错误中间件中,可以直接读取err.statusCode来决定响应状态。

错误处理与类型安全的结合

在TypeScript项目中,catch块的错误参数类型默认为unknown。这意味着不能直接访问err.message,需要先做类型收窄。

try {
    // 可能抛出错误的操作
} catch (err) {
    if (err instanceof Error) {
        console.error(err.message);
    } else {
        console.error("Unknown error", err);
    }
}

这种写法避免了将非Error类型误判为Error。在大型项目中,建议统一封装一个normalizeError函数,将各种形态的抛出物转换为标准的Error对象。

本节课程知识要点

  1. Node.js错误是Error对象的实例,核心属性包括name、message、stack、code。

  2. 错误分为四类:JavaScript原生错误、系统错误、用户自定义错误、断言错误。

  3. try-catch仅适用于同步代码;异步错误需使用回调、Promise或async-await。

  4. 错误优先回调约定:回调第一个参数为错误对象。

  5. fetch不会因HTTP错误状态码而reject,需手动检查response.ok。

  6. 自定义错误类应继承Error,并设置name和可选的statusCode。

  7. 生产环境不应将stack返回给客户端。

  8. TypeScript中catch参数为unknown,需做类型收窄。

练习与思考

  1. 编写一个ConfigMissingError自定义错误类,当读取配置文件失败且错误码为ENOENT时抛出,其他错误码则原样抛出。

  2. 将以下回调风格的代码改写为async-await,并添加try-catch:

fs.readFile("data.txt", function (err, data) {
    if (err) {
        console.error(err);
    } else {
        console.log(data.toString());
    }
});
  1. 思考:为什么在Promise链中,.then里抛出的错误可以被后面的.catch捕获?这与同步代码的try-catch有何异同?

现在框架中的惯用写法

在Express中,错误处理中间件有四个参数(err,req,res,next),必须显式声明四个参数才能被识别为错误处理中间件。在Koa中,通常通过app.on('error')和ctx.throw配合使用。NestJS则提供了ExceptionFilter机制,将错误处理与业务逻辑彻底解耦。无论框架如何变化,核心思路一致:错误应在合适的层级被捕获、转换和记录,最终以统一的格式返回给调用方。

← Node.js定时器教程:setTimeout、setInterval、setImmediate用法与事件循环阶段详解 Node.js DNS模块实战教程:域名解析、记录查询与Promise API全解 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号