Angular NG0204 错误(Invalid Injection Token)完全指南:成因、诊断与修复
NG0204(Invalid Injection Token)是 Angular 依赖注入(Dependency Injection, DI)系统中标识"无法为某 Token 解析出可用的工厂/提供者"的运行时错误,在实际开发中常以"启动即崩溃"的方式出现,排查体验差。本文以本仓库 @angular/core 源码与其官方错误说明为准,先讲清该错误在 DI 解析链路中的触发位置与两条典型报错文案的含义,再逐条拆解最常出现的三种触发场景(缺少服务装饰器、InjectionToken 未配置 Provider、构造器参数不可解析),最后给出可落地的排查清单与修复代码。读完本文,你将能够仅凭报错信息与调用栈,快速定位并修复此类注入失败问题。
NG0204 是什么:一个错误的两个触发点
在 packages/core/src/errors.ts 中,NG0204 对应 RuntimeErrorCode.INVALID_INJECTION_TOKEN = -204,属于 core 包错误区间(100–999)中的"依赖注入类错误",与 CYCLIC_DI_DEPENDENCY (-200)、PROVIDER_NOT_FOUND (-201)、MISSING_INJECTION_CONTEXT (-203) 等同类错误并列。
从报错时机看,NG0204 本质上发生在注入器(Injector)尝试为某个 Token 寻找工厂函数却失败的时刻。核心代码集中在 packages/core/src/di/r3_injector.ts:
injectableDefOrInjectorDefFactory(token):先通过getInjectableDef(token)读取 Token 上的可注入定义(即ɵprov),取不到再尝试getFactoryDef(token)读取工厂定义;- 若两者皆为空,且该 Token 是
InjectionToken实例,则抛出Token ... is missing a ɵprov definition; - 若 Token 只是一个普通的
Function(类),则进入getUndecoratedInjectableFactory(token):此时若构造函数形参个数token.length > 0,说明它存在 Angular 无法隐式解析的依赖,抛出Can't resolve all parameters for X: (?, ?, ?)。
这两条报错文案分别对应本文后面要讲的两类最常见触发场景。值得注意的一个细节是:报错文案中的 ngDevMode && 前缀表明,这些详细的诊断信息属于 dev 模式输出;在开发构建中看到它们,说明问题正出在 Token 的"可注入性"本身,而不是简单的"找不到 Provider"(后者通常是另一错误码 NG0201 的范畴)。
为什么会出现 NG0204:装饰器与 ɵprov 的关系
Angular 之所以能在构造函数参数没有显式标注类型的情况下自动注入,依赖的是编译期/运行期在每个类上生成的可注入元数据(ɵprov)。在 JIT 场景下,这一过程由装饰器编译逻辑完成:
@Service()/@Injectable()装饰器会触发对类型的编译,并把ɵprov定义写到类上。以仓库中的 Service 装饰器实现 为例,autoProvided选项默认为true,表示该服务会自动暴露给 DI 系统;factory选项则允许自定义创建实例的方式,二者均会参与生成最终的可注入定义。- 对应的 JIT 编译逻辑见 packages/core/src/di/jit/service.ts:
compileService会把编译结果(NG_PROV_DEF,即ɵprov)以属性形式挂载到类型上。 - 在 packages/core/src/di/jit/util.ts 中,
reflectDependencies通过反射读取构造器参数元数据,并把@Optional()、@SkipSelf()、@Inject(TOKEN)等装饰器参数翻译成 DI 的依赖元数据——这正是"Angular 依赖 TypeScript 元数据推断参数类型"这一说法的实现位置。
理解了这条链路,就能明白两条核心结论:
- 缺少装饰器的类没有
ɵprov,因此当它恰好又有构造器依赖时,Angular 无法得知如何创建它,于是抛出 NG0204; InjectionToken本身只是描述符,若创建时没有附带工厂函数、又没有在任何注入器上注册 Provider,它同样没有ɵprov,一旦被注入就报 NG0204。
关于 inject() 函数需要单独说明(官方文档中的 NOTE 也强调过):inject() 接收的是显式 Token,因此"构造器参数类型无法推断"这种问题不会直接发生在它身上。但若 inject() 所注入的那个类自身缺少装饰器、且带有自己的构造器依赖,错误依然会出现——本质没变,只是"出错的那个类"换了个位置。
常见触发场景一:类缺少 @Service()(或 @Injectable())装饰器
这是 NG0204 出现频率最高的一种情形:类本身需要依赖注入,却忘了加装饰器。例如:
export class UserClient {
// Angular 无法解析 http —— UserClient 上没有可注入元数据(ɵprov)
constructor(private http: HttpClient) {}
}
此时 Angular 读取不到 UserClient 的任何可注入定义,只能退回到 getUndecoratedInjectableFactory,发现构造器存在形参(http),于是抛出 Can't resolve all parameters for UserClient: (?, ?)。
修复方式就是补上装饰器:
@Service()
export class UserClient {
constructor(private http: HttpClient) {}
}
装饰器生效后,类上被写入了 ɵprov 定义,注入器即可正常解析其构造器依赖。根据本仓库现状,@Service() 已被设计为公开 API(其 autoProvided 默认为 true,会自动将服务暴露给 DI 系统),@Injectable() 同样是本仓库官方错误文档中认可的等价方案。
常见触发场景二:InjectionToken 缺少 Provider 定义
InjectionToken 用来为"非类类型"的依赖(如接口、字符串、配置对象)提供类型安全的 DI Token。但 Token 本身只是一把"钥匙",必须配有可注入定义或 Provider 才能真正被解析。若只创建了 Token 却没有提供值,一旦被注入就会出现错误 Token X is missing a ɵprov definition。
错误示例:
// 只有描述,没有 factory,也没有在任何地方 provide —— 被注入时即报 NG0204
export const APP_CONFIG = new InjectionToken<AppConfig>('app config');
两种标准修复方式:
方式一:在使用处注册 Provider(值为某个具体的值):
import {InjectionToken} from '@angular/core';
export const API_URL = new InjectionToken<string>('api url');
// 在组件/指令/模块的 providers 中注册:
// {provide: API_URL, useValue: 'https://api.example.com'}
方式二:在创建 Token 时直接给出默认工厂函数。查看 InjectionToken 实现 可以发现,当构造参数传入 factory 时,其 providedIn 会默认落到 'root',即 Token 自带根级可注入定义:
export const API_URL = new InjectionToken<string>('api url', {
factory: () => 'https://api.example.com',
});
从排查与复用角度还应当注意 Token 的唯一性:Angular 用 Token 对象本身的引用(而非其描述字符串)来匹配 Provider 与注入点。若在多个文件中分别创建了描述相同但对象不同的 InjectionToken,即使描述完全一致也无法互相解析——这一节在 DI 排查与故障处理指南中有更完整的讨论,建议将 Token 统一导出到共享文件中,避免重复实例化。
常见触发场景三:构造器参数本身不可解析
第三种典型情况是:类已经带有装饰器,但其某个构造器参数类型不是可解析的 DI Token。例如参数引用的是一个既未注册 Provider、也不是可注入类的类型:
@Service()
export class DataStore {
// Angular 无法解析 config:AppConfig 没有任何 Provider,也不可注入
constructor(private config: AppConfig) {}
}
这里的 AppConfig 可能是一个纯 TypeScript 接口(接口在运行时会被擦除,没有实体可作为 Token),也可能是一个忘记注册为 Provider 的普通类。接口类型本身没有运行时标识,无法作为 DI Token 使用;若确需按接口注入,业界通行做法就是改配 InjectionToken(见上一节)。
修复需要遵循两条原则:
- 所有构造器参数要么是可注入类(带装饰器、有
ɵprov),要么已在某层注入器注册了对应 Provider; - 确实可能缺失的依赖应显式声明为可选,例如给参数标注
@Optional(),或结合@Inject()指定显式 Token。
对可选依赖使用 @Optional() 可以让 Angular 在找不到 Provider 时注入 null 而不是抛错,从而把"缺失依赖"从编译期/启动期错误降级为可运行的业务逻辑分支。
读懂报错文案与调用栈
NG0204 的报错信息通常直接点明问题 Token,两种主要文案及其含义对照如下:
| 报错文案 | 含义与处理方向 |
|---|---|
Can't resolve all parameters for X: (?, ?, ?) |
类 X 缺少装饰器或其依赖参数无法被推断。? 占位符的数量等于无法解析的形参个数。检查类是否带有 @Service() / @Injectable(),并确保每个形参类型都有 Provider。 |
Token X is missing a ɵprov definition |
X 是一个没有可注入定义的 InjectionToken(或装饰器未生效的类 Token)。用 {provide: TOKEN, useValue: ...} 注册,或在 Token 定义上补充默认 factory。 |
注意第一行文案中的 ? 数量与构造器形参个数一一对应——它由 getUndecoratedInjectableFactory 中用 newArray(paramLength, '?') 生成(见 r3_injector.ts 中的实现),paramLength 来自 JS 函数的 length 属性。若类本身没有构造器参数,Angular 甚至可以不依赖装饰器直接实例化(无参函数场景被单独放行),这也解释了为什么"报错的类总是恰好带构造器依赖"。
系统化排查清单
拿到报错后,可以按下面顺序从调用栈自底向上排查,定位到发生注入的位置后逐项核对:
- 该类是否带有
@Service()/@Injectable()装饰器——缺失则补齐,并确认没有被误删或从错误的模块导入; - 构造器每个形参是否都有对应 Provider——检查形参类型是否是已注册的可注入类,或是否在组件/指令/
forRoot()/providers等处配置了对应提供者; - 每个
InjectionToken是否已有 Provider 或默认值——要么在使用侧配置{provide: TOKEN, useValue: ...}/useFactory/useClass,要么在创建 Token 时提供factory默认值; - Token 是否唯一——确认是从共享文件统一导入,而不是在多个地方各自
new InjectionToken(...); - 确属可选的依赖是否加了
@Optional()——避免本可优雅降级为null的依赖变成硬错误。
仓库内的相关单元测试覆盖了上述行为(如 packages/core/test/di/r3_injector_spec.ts 与 packages/core/test/linker/ng_module_integration_spec.ts 中均涉及 NG0204 / missing a ɵprov definition 的用例),当你修改相关实现时,这些测试也是验证行为是否回归的可靠参考。此外,官方错误索引 errors/overview.md 收录了本错误的完整清单,DI 实战侧的补充建议可继续阅读 debugging-and-troubleshooting-di.md 的 "InjectionToken issues" 小节;本文的直接依据文档为 NG0204.md。
小结
NG0204 不是偶发的随机故障,它的出现几乎总是意味着某个 DI Token(类或 InjectionToken)缺少"可被注入系统认识的元数据"。把握住三个要点即可系统性地消灭此类错误:类务必携带装饰器、Token 务必有 Provider 或默认工厂、构造器形参务必可解析。而一旦报错出现,先读文案(是 Can't resolve all parameters 还是 missing a ɵprov definition),再顺调用栈找到注入发生处,配合本文清单逐项核对,通常几分钟内就能锁定并修复问题。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00