丁同亚的博客
夺朱非正色

NestJS 中 instanceof 检查失效导致异常处理错误 (js原生对象不影响 )

问题描述

在 NestJS 微服务项目中,使用全局异常过滤器 HttpExceptionFilter 捕获异常时,发现 UnauthorizedException 虽然继承自 HttpException,但 instanceof 检查返回 false,导致异常被误判为未知异常,返回错误的错误码。

现象

期望行为

{
  "success": false,
  "data": null,
  "errorCode": 1002,
  "errorMessage": "无效的令牌"
}

实际行为

{
  "success": false,
  "data": null,
  "errorCode": 1000,
  "errorMessage": "Unauthorized"
}

调试发现

通过调试发现以下异常特征:

  1. 异常对象的 [[Prototype]] 正确显示为 HttpException
  2. exception.name"UnauthorizedException"
  3. exception instanceof HttpException 返回 false

根本原因

模块实例化问题

在 Monorepo 或多模块项目中,@nestjs/common 被多次实例化,导致不同模块中的类引用不一致。

// api-gateway 模块中的 HttpException
const HttpException_A = require('@nestjs/common').HttpException;

// shared-config 模块中的 HttpException
const HttpException_B = require('@nestjs/common').HttpException;

// 虽然是同一个包,但是不同的实例
HttpException_A !== HttpException_B; // true

// UnauthorizedException 继承自 HttpException_A
const ex = new UnauthorizedException();

// 在 shared-config 中检查
ex instanceof HttpException_B; // false ❌

instanceof 的工作原理

obj instanceof Constructor
// 等价于
Constructor.prototype.isPrototypeOf(obj)

instanceof 检查依赖于构造函数的引用相等性,当构造函数来自不同模块实例时,检查会失败。

技术背景

项目结构

  • api-gateway: API 网关服务
  • main-service: 主服务(用户服务)
  • other-service: 其他服务(通知服务)
  • shared-config: 共享配置包(本地包)
  • web: 前端应用

依赖关系

api-gateway
  └── @nest-m/shared-config (本地包)

shared-config
  └── @nestjs/common

api-gateway
  └── @nestjs/common

这导致 @nestjs/common 在两个不同的上下文中被加载。

解决方案

方案 1:使用 name 属性检查(推荐)

不依赖 instanceof,而是检查异常的 name 属性:

// 处理 instanceof 失败但实际是 HttpException 的情况
else if (
  exception instanceof Error &&
  ['UnauthorizedException', 'BadRequestException', 'NotFoundException',
   'ForbiddenException', 'ConflictException'].includes(exception.name)
) {
  const exceptionObj = exception as any;
  status = exceptionObj.getStatus ? exceptionObj.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
  errorCode = this.getErrorCodeFromStatus(status);
  errorMessage = exception.message;
  this.logger.error(
    `[${errorCode}] ${errorMessage}`,
    `${request.method} ${request.url}`,
  );
}

优点

  • ✅ 不依赖 instanceof
  • ✅ 不受模块实例化问题影响
  • ✅ 性能更好
  • ✅ 代码更清晰

方案 2:检查原型链

else if (exception instanceof Error) {
  const httpException = require('@nestjs/common').HttpException;
  if (httpException.prototype.isPrototypeOf(exception)) {
    status = exception.getStatus();
    errorCode = this.getErrorCodeFromStatus(status);
    errorMessage = exception.message;
  }
}

优点

  • ✅ 检查原型链,更准确
  • ✅ 不依赖构造函数引用

缺点

  • ❌ 仍需动态导入
  • ❌ 代码稍复杂

方案 3:统一依赖版本(治本)

确保所有服务使用相同版本的 @nestjs/common

# 使用 npm workspaces
npm workspaces

# 或使用 pnpm workspace
pnpm workspace

# 或在根目录的 package.json 中统一管理
{
  "workspaces": [
    "api-gateway",
    "main-service",
    "other-service",
    "shared-config"
  ]
}

优点

  • ✅ 治本方案
  • ✅ 避免依赖重复

缺点

  • ❌ 需要重构项目结构
  • ❌ 可能影响现有构建流程

经验教训

1. Monorepo 中的模块实例化问题

在 Monorepo 项目中,共享包可能导致依赖被多次实例化,需要特别注意类的引用一致性。

2. instanceof 的局限性

instanceof 依赖于构造函数的引用相等性,在模块化环境中可能不可靠,尤其是在以下场景:

  • Monorepo 项目
  • 使用本地共享包
  • 微服务架构
  • Webpack 打包

3. 防御性编程

在异常处理中使用多种检查方式,避免单点失效:

// 组合多种检查方式
if (exception instanceof HttpException) {
  // 方式1: instanceof
} else if (
  exception instanceof Error &&
  ['UnauthorizedException'].includes(exception.name)
) {
  // 方式2: name 属性
} else if (
  exception instanceof Error &&
  HttpException.prototype.isPrototypeOf(exception)
) {
  // 方式3: 原型链
}

4. 调试技巧

使用 Object.getPrototypeOf() 检查原型链,比 instanceof 更可靠:

const proto = Object.getPrototypeOf(exception);
console.log('Prototype:', proto?.constructor?.name);
console.log('Is HttpException?', HttpException.prototype.isPrototypeOf(exception));

适用场景

  • ✅ NestJS Monorepo 项目
  • ✅ 使用本地共享包(shared-config)
  • ✅ 微服务架构
  • ✅ 需要统一异常处理的场景
  • ✅ 使用 Webpack 或其他打包工具的项目

相关资源


总结:在模块化项目中,instanceof 检查可能因为模块实例化问题而失效。使用 name 属性或原型链检查是更可靠的替代方案。在生产环境中,建议使用组合检查方式,确保异常处理的健壮性。

posted on 2026-01-23 17:04  丁同亚的博客  阅读(0)  评论(0)    收藏  举报