首页
/ Angular NG0201 "No Provider Found" 错误全解析:成因、定位与修复实战

Angular NG0201 "No Provider Found" 错误全解析:成因、定位与修复实战

2026-09-07 13:43:06作者:董斯意

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.tsthrowProviderNotFoundError

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.tsnotFoundValueOrThrow 中传入 '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.tspackages/core/test/di/r3_injector_spec.tspackages/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 的那个对象)向前倒推

  1. 先看报错文本中 No provider for ... 后面的 token 是谁,回到代码中找出它被注入的位置(构造函数参数或 inject() 调用处)。
  2. 判断该注入发生在哪个上下文:是服务(Module/Environment Injector 链)还是组件/指令(NodeInjector 链)。结合上文,报错文本是否带 "in NodeInjector" 能帮你快速分流。
  3. 检查"从注入点向上直到根注入器"这条路径上,是否有任何一个注入器注册了该 token:依次排查组件 providers@NgModule({providers})bootstrapApplication / ApplicationConfigproviders,以及服务类自身是否带 @Service() / @Injectable({providedIn: 'root'})
  4. 特别留意懒加载模块/路由级 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 NgModule or has the @Service decorator 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),而不是把它们写进 ApplicationConfigproviders 数组——这样打包优化工具才能做 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 理解注入器层级关系。

常见遗漏点自查清单

  1. 服务漏标装饰器:最常见的根因——类上既没有 @Service(),也没有 @Injectable(),也不在任何 providers 中。详见 guide/di/debugging-and-troubleshooting-di.md 中 "Missing the @Service or @Injectable decorator" 一节。
  2. 只写了 @Injectable() 却没写 providedIn:它只是声明"可被注入",并不会自动注册。要么补上 providedIn: 'root',要么改用 @Service(),要么把它放进某个 providers 数组。
  3. 作用域不匹配:服务注册在组件级、却在组件之外的指令/服务里注入,或跨懒加载边界注入,都可能触发 NG0201。
  4. token 拼写/导入不一致:注入用的 token 与实际注册的 token(尤其使用 InjectionToken 时)不是同一个引用。
  5. EnvironmentProviders 放错位置:从源码 errors_di.ts 可见,把 EnvironmentProviders(如 importProvidersFrom 的返回值)放进组件 providers 会抛出 PROVIDER_IN_WRONG_CONTEXT(NG0207)——这是与 NG0201 相邻但原因不同的 DI 错误,排查时注意区分。

结语

NG0201 的报错信息本身已足够"自解释":它精确地告诉你哪一个 token 没有被找到,剩下的工作就是从注入点沿注入器链向上核对 Provider 注册位置。借助本文梳理的三条注册途径与倒推式排查法,绝大多数场景都可以在几分钟内解决。若希望进一步系统性掌握依赖注入,推荐阅读仓库内的 guide/di/overview.mdguide/di/creating-and-using-services.mdguide/di/debugging-and-troubleshooting-di.md,以及相邻错误文档 NG0200(循环依赖)与 NG0203(在非法上下文调用 inject())作对照。

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