首页
/ Angular NG0203 错误排查指南:为什么 `inject()` 必须在注入上下文中调用,以及如何修复

Angular NG0203 错误排查指南:为什么 `inject()` 必须在注入上下文中调用,以及如何修复

2026-09-07 11:58:56作者:乔或婵

NG0203 是 Angular 依赖注入(Dependency Injection)体系中最常见的运行时错误之一,触发时的报错信息为 "inject() must be called from an injection context"。这篇指南以 adev/src/content/reference/errors/NG0203.md 官方错误手册为核心,结合本仓库 packages/core 中的 DI 实现源码,讲清注入上下文(injection context)的边界、合法与非法调用位置、报错在源码中的产生机制,并给出可直接落地的调试与修复方案。读完你既能看明白栈信息,也能在工程里快速把错误的 inject() 挪到正确位置。

NG0203 到底是什么

NG0203 表示你尝试在不允许的注入上下文之外调用 Angular 的 inject() 函数。注入上下文只在类的创建与初始化期间可用;此外,被 runInInjectionContext 包裹的函数体内也可用。

从本仓库源码看,该错误并非某一段独立文案,而是由 packages/core 中定义的统一错误码触发。在 packages/core/src/errors.ts 中可以看到:

MISSING_INJECTION_CONTEXT = -203,

该枚举上方的注释说明了一个约定:负数错误码表示该错误在 angular.dev 上有一份详细指南,取绝对值即为文档编号 203,格式化后就是 NG0203。真正把错误抛出来的有两处,都使用了同一个错误码:

  • packages/core/src/di/injector_compatibility.tsinjectInjectorOnly:当 getCurrentInjector() 返回 undefined(即当前没有任何可用的注入器上下文)时抛出,其 ngDevMode 文案为:

    The token token injection failed. inject() function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext.

  • packages/core/src/di/contextual.tsassertInInjectionContext:先调用 isInInjectionContext() 做前置断言,失败时抛出同样的 MISSING_INJECTION_CONTEXT 错误码,并把发起调用的函数名拼进消息,方便你定位是哪个 API 越过了边界。

inject() 函数本体同样定义在 injector_compatibility.ts,最终通过 getInjectImplementation() || injectInjectorOnly 的路径执行,因此「当前是否处于注入上下文」直接决定了它能不能成功。注意这些消息只在开发模式下(ngDevMode)完整输出,生产构建中会被摇树,你主要应靠堆栈定位问题。

什么是「注入上下文」:源码里的判定逻辑

Angular 文档把注入上下文描述为「类被 DI 系统创建与初始化」的过程区间。想理解它为何在生命周期钩子里失效,需要看上下文的实际判定逻辑。在 contextual.ts 中:

export function isInInjectionContext(): boolean {
  return getInjectImplementation() !== undefined || getCurrentInjector() != null;
}

也就是说,Angular 用一个「当前注入器」的全局指针来判断你此刻是否在上下文内。只有 DI 系统正在实例化一个类、正在执行 provider 的工厂函数、或正在执行 runInInjectionContext 包裹的同步代码时,这个指针才会被设置;一旦类实例创建完成(例如组件已经进入生命周期),注入器指针就会被恢复,此时再调用 inject() 便会命中 NG0203。

所以可以总结出两个核心要点:

  1. 上下文是"同步的"inject() 只能在同步执行栈中使用,不能出现在异步回调或 await 之后——因为在那些时刻 DI 早已离开了当前执行帧。这一约束同样记录在 contextual.tsrunInInjectionContext 的文档注释里。
  2. 类实例的创建窗口很窄:只有「被 DI 实例化」的类(@Injectable@Component 等)在构造阶段才天然处于上下文中。

官方对允许位置的完整描述可参见 dependency-injection-context.md,其明确列出的上下文包括:被 DI 系统实例化的类的构造阶段(如 @Injectable@Component)、这些类的字段初始化器useFactory 指定的工厂函数、@InjectableInjectionTokenfactory 函数。而本仓库 injector_compatibility.tsinject()@usageNotes 也逐条印证了这五类合法位置——官方错误页正是从这里提炼的。

合法的 inject() 调用位置

1. 构造函数体、构造参数与字段初始化器

inject() 最常见的合法位置,是类被 DI 创建期间的三类代码:构造函数体、构造函数参数、字段初始化器。这是官方错误页给出的原版示例:

@Service()
export class Car {
  radio: Radio | undefined;

  // OK: 字段初始化器
  spareTyre = inject(Tyre);

  constructor() {
    // OK: 构造函数体
    this.radio = inject(Radio);
  }
}

字段初始化器之所以合法,是因为它在引擎语义上发生于构造流程之中;而构造函数参数位置依赖注入(例如 constructor(engine: Engine))本质上是更古老的等价写法。二者在同一实例化周期内,注入器指针都处于激活状态。

2. Provider 的工厂函数

从 provider 的工厂函数中调用 inject() 同样合法。官方示例:

providers: [
  {
    provide: Car,
    useFactory: () => {
      // OK: 类工厂函数
      const engine = inject(Engine);
      return new Car(engine);
    },
  },
];

工厂函数之所以安全,是因为它本身就是 DI 系统解析 provider 时执行的代码——此时解析流程尚未结束,注入上下文仍然有效。同一机制也适用于 InjectionTokenfactory 字段,这也是为什么 dependency-injection-context.md 会把它单列为一种合法上下文。

3. runInInjectionContext 包裹的函数体

即使不在类的实例化过程中,你也可以主动创建上下文。全局函数 runInInjectionContext(injector, fn) 能在给定 Injector 的上下文内同步执行 fn,让其中的 inject() 正常工作。其实现位于 contextual.ts:执行前保存并设置当前注入器与 profiler 上下文,执行后通过 try/finally 恢复原状——所以它只保证 fn 同步执行期间可用。若你把回调异步派发出去,等于逃出了这个执行帧,inject() 依然会抛 NG0203。

值得一提的坑:R3Injector 上旧的 injector.runInContext(...) 方法已在 r3_injector.ts 中标记为 deprecated,官方推荐统一使用独立的 runInInjectionContext 替代。

非法的调用位置:NG0203 的高发场景

调用发生在类实例创建之后、且没有 runInInjectionContext 保护时,必然触发 NG0203。官方错误页特别点名了**方法(包括生命周期钩子)**这一高发场景:

@Component({ ... })
export class Car {
  ngOnInit() {
    // ERROR: 太迟了,组件实例早已创建完成
    const engine = inject(Engine);
    engine.start();
  }
}

只要把这段代码对照源码走一遍就明白原因:ngOnInit 触发时,Angular 早已完成了 Car 的实例化并恢复了「当前注入器」指针,injectInjectorOnlyinjector_compatibility.ts 中读到 undefined,随即抛出 MISSING_INJECTION_CONTEXT(即 NG0203)。

同类的高发位置还包括:事件回调、setTimeout/Promise 回调、RxJS 订阅回调、effect()/computed() 之外被延迟执行的辅助函数,以及把 inject() 抽到普通工具函数里又在非上下文中调用。所有这些本质上都是「逃出了类实例化的同步窗口」。

如何调试与修复 NG0203

诊断:沿堆栈回推

官方建议的调试手法是:从错误的堆栈信息往回走(work backwards from the stack trace),找到那个不合规的 inject() 调用点——报错堆栈会标记出调用 inject() 的那一帧,顺着它找到你代码里真正发起调用的位置。定位时要留意,报错可能在库内部帧,但问题代码永远是你自己的那一个调用点

修复:把调用移到合法位置

最直接的修复是inject() 挪到允许的位置——通常是构造函数或字段初始化器。例如上文的组件可改写为:

@Component({ ... })
export class Car {
  // 用字段初始化器在实例化期完成注入
  private readonly engine = inject(Engine);

  ngOnInit() {
    this.engine.start();
  }
}

如果依赖的解析时机确实要求「组件已就绪后」再取值(例如依赖 ElementRef 之外的运行时状态),一个稳妥的替代模式是:在构造期先注入 InjectorEnvironmentInjector 并保存为字段,等需要时再用 runInInjectionContext(this.injector, () => ...) 同步创建上下文执行注入。这样既绕过了 NG0203,又把注入点的生命周期显式化。

测试场景提示:如果 NG0203 出现在单元测试里(例如直接调用了一个内部使用 inject() 的辅助函数),别忘了测试工具提供了专门的通道 TestBed.runInInjectionContext,可以让 inject() 在测试上下文内成功执行:

TestBed.runInInjectionContext(() => {
  // ...
});

该 API 在本仓库的 DI 测试中被广泛使用,例如 di_spec.ts 中反复用它配合 runInInjectionContext(injector, () => inject(TOKEN)) 验证在任意 Injector 上下文内解析 token 的行为,可把它当作测试辅助函数的准绳示例。

一张速查表:允许 vs 禁止

调用位置 是否允许 原因
构造函数体 / 构造参数 类仍处于 DI 实例化阶段
字段初始化器 发生在构造流程中
Provider useFactory 工厂 DI 解析 provider 时执行
InjectionTokenfactory 同上,解析窗口内执行
runInInjectionContext 包裹的同步函数 显式设置了当前注入器
生命周期钩子(如 ngOnInit 实例已创建,注入器指针已恢复
普通方法、事件回调、异步回调 逃出了同步的实例化窗口
await 之后的代码 上下文只覆盖同步执行帧

小结

NG0203 本质上是 Angular DI 对 inject() 调用边界的一次运行时守卫:错误码 MISSING_INJECTION_CONTEXT = -203 定义于 errors.ts,实际抛错逻辑位于 injector_compatibility.tscontextual.ts。记住「类创建与初始化的同步窗口」这一判断主线,配合错误页给出的修法——把调用移进构造函数或字段初始化器、必要时用 runInInjectionContext 兜底、测试里用 TestBed.runInInjectionContext——就能快速终结这一类报错。

登录后查看全文
热门项目推荐
相关项目推荐