Angular NG0203 错误排查指南:为什么 `inject()` 必须在注入上下文中调用,以及如何修复
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.ts 中
injectInjectorOnly:当getCurrentInjector()返回undefined(即当前没有任何可用的注入器上下文)时抛出,其 ngDevMode 文案为:The
tokentoken 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 withrunInInjectionContext. -
packages/core/src/di/contextual.ts 中
assertInInjectionContext:先调用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。
所以可以总结出两个核心要点:
- 上下文是"同步的":
inject()只能在同步执行栈中使用,不能出现在异步回调或await之后——因为在那些时刻 DI 早已离开了当前执行帧。这一约束同样记录在 contextual.ts 中runInInjectionContext的文档注释里。 - 类实例的创建窗口很窄:只有「被 DI 实例化」的类(
@Injectable、@Component等)在构造阶段才天然处于上下文中。
官方对允许位置的完整描述可参见 dependency-injection-context.md,其明确列出的上下文包括:被 DI 系统实例化的类的构造阶段(如 @Injectable 或 @Component)、这些类的字段初始化器、useFactory 指定的工厂函数、@Injectable 与 InjectionToken 的 factory 函数。而本仓库 injector_compatibility.ts 中 inject() 的 @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 时执行的代码——此时解析流程尚未结束,注入上下文仍然有效。同一机制也适用于 InjectionToken 的 factory 字段,这也是为什么 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 的实例化并恢复了「当前注入器」指针,injectInjectorOnly 在 injector_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 之外的运行时状态),一个稳妥的替代模式是:在构造期先注入 Injector 或 EnvironmentInjector 并保存为字段,等需要时再用 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 时执行 |
InjectionToken 的 factory |
✅ | 同上,解析窗口内执行 |
runInInjectionContext 包裹的同步函数 |
✅ | 显式设置了当前注入器 |
生命周期钩子(如 ngOnInit) |
❌ | 实例已创建,注入器指针已恢复 |
| 普通方法、事件回调、异步回调 | ❌ | 逃出了同步的实例化窗口 |
await 之后的代码 |
❌ | 上下文只覆盖同步执行帧 |
小结
NG0203 本质上是 Angular DI 对 inject() 调用边界的一次运行时守卫:错误码 MISSING_INJECTION_CONTEXT = -203 定义于 errors.ts,实际抛错逻辑位于 injector_compatibility.ts 与 contextual.ts。记住「类创建与初始化的同步窗口」这一判断主线,配合错误页给出的修法——把调用移进构造函数或字段初始化器、必要时用 runInInjectionContext 兜底、测试里用 TestBed.runInInjectionContext——就能快速终结这一类报错。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00