Angular NG0201 "No Provider Found" 错误全解析:成因、定位与修复实战
NG0201 是 Angular 依赖注入(Dependency Injection,DI)体系中最常见的运行时错误之一:当你尝试注入某个服务(或其它 token),却在整个可用的注入器(Injector)链路中都没有找到对应的 Provider 时,Angular 就会抛出它。本文基于 Angular 官方源码与错误参考文档 NG0201.md,从错误代码的生成原理讲起,逐步拆解它的典型触发场景、消息格式差异与排错思路,并给出在当前版本(支持 @Service() 新装饰器的 DI 体系)下最推荐的修复写法,帮助你做到"看报错即可定位、改一处即可恢复"。
Provider:理解 NG0201 之前必须先理解的概念
在 Angular 中,"Provider(提供者)"是一张映射表:把一个 token(通常是类或 InjectionToken)映射为可被注入的值。当你把某个服务注入到类的构造函数或 inject() 调用中时,Angular 会按照注入器层级去寻找这个映射。若查找失败,DI 引擎就把失败上报为 NG0201。
关于 Provider 的完整介绍可参阅 guide/di/overview.md(依赖注入总览)与 guide/di/creating-and-using-services.md(创建与使用服务)。
错误参考文档原文对这一现象的描述非常直接:
You see this error when you try to inject a service but have not declared a corresponding provider.
即:注入行为发生,但声明(Provider)缺失。文档还特别指出一个高频场景:"This is commonly thrown in services, which require non-existing providers."——即服务 A 的构造函数里注入了服务 B,但 B 从未在任何注入器中注册,导致 A 在实例化时抛错。
从源码看 NG0201 是如何产生的
错误码 -201 与消息格式化
在 packages/core/src/errors.ts 中,Dependency Injection 错误区定义了一个特殊常量:
// Dependency Injection Errors
CYCLIC_DI_DEPENDENCY = -200,
PROVIDER_NOT_FOUND = -201, // ← NG0201 对应的运行时错误码
INVALID_FACTORY_DEPENDENCY = 202,
注意该值是负数。源码注释说明:负号是一个特殊标记,表示"这个错误带有详细指导页"。在 formatRuntimeError 中,负错误码会被格式化成 NG0${Math.abs(code)},即 NG0201,并在开发模式(ngDevMode)下自动追加一行 "Find more at .../NG0201" 的引导链接。
两条实际报错文本对应两个不同抛出点
在仓库测试与实现中,NG0201 有两种大同小异的消息措辞,理解它们的差异有助于快速判断"是哪一层注入器失败了":
其一:模块级(Module/Root)注入链走到尽头——NullInjector
当整个注入器链(从组件往上直到根)都无法找到 Provider 时,最后会落到 NullInjector。在 packages/core/src/di/null_injector.ts 中:
export class NullInjector implements Injector {
get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any {
if (notFoundValue === THROW_IF_NOT_FOUND) {
const message = ngDevMode ? `No provider found for \`${stringify(token)}\`.` : '';
const error = createRuntimeError(message, RuntimeErrorCode.PROVIDER_NOT_FOUND);
error.name = 'ɵNotFound';
throw error;
}
return notFoundValue;
}
}
对应运行时的报错形如:
NG0201: No provider found for `MyService`. Find more at https://angular.dev/errors/NG0201
其二:元素级(NodeInjector)解析失败——throwProviderNotFoundError
当在组件/指令所在的 NodeInjector 上查找失败时,会走到 packages/core/src/render3/errors_di.ts 的 throwProviderNotFoundError:
export function throwProviderNotFoundError(
token: ProviderToken<unknown>,
injectorName?: string,
): never {
const errorMessage =
ngDevMode &&
`No provider for ${stringifyForError(token)} found${injectorName ? ` in ${injectorName}` : ''}`;
throw new RuntimeError(RuntimeErrorCode.PROVIDER_NOT_FOUND, errorMessage);
}
调用方在 packages/core/src/render3/di.ts 的 notFoundValueOrThrow 中传入 'NodeInjector':
function notFoundValueOrThrow<T>(
notFoundValue: T | null,
token: ProviderToken<T>,
flags: InternalInjectFlags,
): T | null {
if (flags & InternalInjectFlags.Optional || notFoundValue !== undefined) {
return notFoundValue; // @Optional() 或显式给了默认值 → 不抛错
} else {
throwProviderNotFoundError(token, 'NodeInjector');
}
}
对应运行时的报错形如:
NG0201: No provider for MyService found in NodeInjector. Find more at https://angular.dev/errors/NG0201
仓库中的集成测试(如 packages/core/test/linker/ng_module_integration_spec.ts、packages/core/test/di/r3_injector_spec.ts、packages/core/test/linker/view_injector_integration_spec.ts)正是用正则断言上述两种文本格式,可作为复现与验证的参考。
小结:报错里若包含 "in NodeInjector",说明问题出在组件/指令所在的元素级注入器查找路径上;若不包含(纯 "No provider found for ..."),说明注入链一路向上直到 NullInjector(根级)都没命中。二者本质相同——token 未在可触及的注入器作用域内注册。
为什么"服务里注入了不存在的服务"最容易触发
DI 是递归解析的:实例化服务 A 时若其构造函数依赖 B,Angular 必须先解析 B。参考文档强调 NG0201 常见于"service 依赖 service"的场景,这与源码结构吻合——依赖链越长,中间任何一个环节漏注册,错误都会在解析到那一环时炸出,而报错信息里的 token 正是第一个没找到的依赖(即 ${this} 指向的目标)。
按参考文档给出的调试方法:倒推法
参考文档 NG0201.md 明确给出了排错总思路:
Work backwards from the object where the error states that a provider is missing:
No provider for ${this}!.
即从报错点(缺失 Provider 的那个对象)向前倒推:
- 先看报错文本中
No provider for ...后面的 token 是谁,回到代码中找出它被注入的位置(构造函数参数或inject()调用处)。 - 判断该注入发生在哪个上下文:是服务(Module/Environment Injector 链)还是组件/指令(NodeInjector 链)。结合上文,报错文本是否带 "in NodeInjector" 能帮你快速分流。
- 检查"从注入点向上直到根注入器"这条路径上,是否有任何一个注入器注册了该 token:依次排查组件
providers、@NgModule({providers})、bootstrapApplication/ApplicationConfig的providers,以及服务类自身是否带@Service()/@Injectable({providedIn: 'root'})。 - 特别留意懒加载模块/路由级 EnvironmentInjector:懒加载会创建新的注入器作用域,若服务只注册在根或父模块而子模块也定义了同名独立注入器,跨作用域注入会失败。可参考 guide/di/lazy-loading-services.md。
另一条实用的补充思路:在排错阶段可以先用 @Optional() 把"可有可无"的依赖临时标记为可选,让应用跳过崩溃、暴露出后续逻辑问题,再决定是为它补 Provider 还是保持可选。从源码看,这正是 InternalInjectFlags.Optional 分支(见上文 notFoundValueOrThrow)所对应的语义:当使用了 @Optional() 且未提供默认值时,DI 返回 null 而不是抛 NG0201。
修复方案:三种"注册 Provider"的途径
参考文档给出的修复核心是:
To fix the error ensure that your service is registered in the list of providers of an
NgModuleor has the@Servicedecorator at top.
下面把这条指引展开为当前仓库代码环境下可直接照做的三种途径。
方案一(推荐):直接给服务类加 @Service() 装饰器
在错误参考文档与当前版本指南中,最简洁的默认解是:
import {Service} from '@angular/core';
@Service()
export class MyService {}
@Service() 会自动把该类注册到根 EnvironmentInjector,使其在整个应用可用,同时不要求你出现在任何 providers 数组里。它的装饰器类型定义位于 packages/core/src/di/service.ts,核心语义是:
- 默认
autoProvided: true,即"自动暴露到 DI 系统"; - 支持
@Service({autoProvided: false})关闭自动注册,此时你必须像普通@Injectable()一样把它手动放进某个providers数组(典型场景是希望把服务作用域限定到某个组件或路由); - 还支持
@Service({factory: () => ...})形式的工厂配置。
关于 @Service 与 @Injectable 的取舍(构造注入 vs inject()、useClass/useFactory 等高级配置差异)可参考 guide/di/creating-and-using-services.md。
方案二(传统写法):@Injectable({providedIn: 'root'})
如果你使用的是经典写法,等价形式是:
import {Injectable} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {}
@Service() 本质就是这一写法的现代简写:二者都会在根注入器提供服务、都支持 tree-shaking(从未被注入时不会进入产物包)。当前仓库的 DI 指南(guide/di/hierarchical-dependency-injection.md)明确建议:当不需要把服务限定在特定作用域时,优先用装饰器(@Service() 或 providedIn),而不是把它们写进 ApplicationConfig 的 providers 数组——这样打包优化工具才能做 tree-shaking。
补充事实依据:源码注释 "Classes with zero-argument constructors can work without
@Service(), but this is not recommended"(见 guide/di/debugging-and-troubleshooting-di.md)。一个没有构造依赖的类即使漏写装饰器也可能"碰巧"能跑,一旦日后加入依赖就会突然抛出 NG0201,因此务必为每个服务都标注装饰器,避免踩坑。
方案三:把服务手动注册进某个 providers 数组
当服务需要限定作用域(而非全局单例)时,把它加进相应层级的 providers:
// NgModule 传统写法
@NgModule({
providers: [MyService], // 本模块注入器提供
// ...
})
export class FeatureModule {}
// 现代独立组件/应用写法
@Component({
selector: 'app-example',
providers: [MyService], // 每个组件实例获得独立实例
// ...
})
export class ExampleComponent {}
// standalone bootstrap 写法
bootstrapApplication(App, {
providers: [
// 全局/环境级 provider,等价于根注册
],
});
请依据"报错发生在哪一层"选择注册层:若某服务只在特定组件树内使用,注册到组件 providers 最合理;若被懒加载模块使用,应保证注册在能被该模块注入器看见的祖先层级(根或共享模块),并参照 guide/di/hierarchical-dependency-injection.md 理解注入器层级关系。
常见遗漏点自查清单
- 服务漏标装饰器:最常见的根因——类上既没有
@Service(),也没有@Injectable(),也不在任何providers中。详见 guide/di/debugging-and-troubleshooting-di.md 中 "Missing the@Serviceor@Injectabledecorator" 一节。 - 只写了
@Injectable()却没写providedIn:它只是声明"可被注入",并不会自动注册。要么补上providedIn: 'root',要么改用@Service(),要么把它放进某个providers数组。 - 作用域不匹配:服务注册在组件级、却在组件之外的指令/服务里注入,或跨懒加载边界注入,都可能触发 NG0201。
- token 拼写/导入不一致:注入用的 token 与实际注册的 token(尤其使用
InjectionToken时)不是同一个引用。 - 把
EnvironmentProviders放错位置:从源码 errors_di.ts 可见,把EnvironmentProviders(如importProvidersFrom的返回值)放进组件providers会抛出PROVIDER_IN_WRONG_CONTEXT(NG0207)——这是与 NG0201 相邻但原因不同的 DI 错误,排查时注意区分。
结语
NG0201 的报错信息本身已足够"自解释":它精确地告诉你哪一个 token 没有被找到,剩下的工作就是从注入点沿注入器链向上核对 Provider 注册位置。借助本文梳理的三条注册途径与倒推式排查法,绝大多数场景都可以在几分钟内解决。若希望进一步系统性掌握依赖注入,推荐阅读仓库内的 guide/di/overview.md、guide/di/creating-and-using-services.md 与 guide/di/debugging-and-troubleshooting-di.md,以及相邻错误文档 NG0200(循环依赖)与 NG0203(在非法上下文调用 inject())作对照。
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 StartedRust0627
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