Angular Core 运行时错误机制解析:从 errors.api.md 读懂 RuntimeError 与错误码体系
导读
本文以 Angular 仓库中由 API Extractor 生成的公开 API 报告 goldens/public-api/core/errors.api.md 为主体,系统拆解 Angular @angular/core 包运行时错误处理机制的完整骨架:RuntimeError 错误类、formatRuntimeError / formatRuntimeErrorCode 格式化函数,以及包含近百个错误码的 RuntimeErrorCode 枚举。读者读完本文后,将能够准确解读浏览器控制台中形如 NG0200: ...、NG0500: ... 的错误编号含义、掌握负号错误码与错误详情页之间的映射规则,理解各功能域(变更检测、依赖注入、模板、水合、信号等)的错误码区间划分,并学会在自己的开发与排错过程中快速定位对应源码与使用方式。
一、errors.api.md 是什么:一份由构建生成的 API 契约
goldens/public-api/core/errors.api.md 是 Angular 通过 API Extractor 生成的 API 报告文件(golden file),其文件头明确声明:
Do not edit this file. It is a report generated by API Extractor.
它的作用主要有三点:
- 公共 API 边界声明:精确记录
@angular/core对外暴露的错误相关符号及其签名,防止贡献者在重构时无意破坏公共 API 的稳定性; - 类型契约快照:任何对
RuntimeError、formatRuntimeError、formatRuntimeErrorCode或RuntimeErrorCode的签名变更都会在 CI 中被 golden 测试捕获; - 权威参考清单:为开发者提供一份"官方认可的"错误码全集,是排查运行时错误的索引表。
报告中声明的全部公共符号只有四个,但错误码枚举本身构成了这份文档的绝大部分价值:
| 符号 | 类型 | 说明 |
|---|---|---|
formatRuntimeError<T extends number = RuntimeErrorCode> |
函数 | 将错误码与消息格式化为标准错误文本 |
formatRuntimeErrorCode<T extends number = RuntimeErrorCode> |
函数 | 将数字错误码转换为 NG0xxx 格式字符串 |
RuntimeError<T extends number = RuntimeErrorCode> |
类 | 运行时错误对象,继承自原生 Error,携带结构化 code |
RuntimeErrorCode |
const enum | 全部运行时错误码的常量枚举 |
对应实现位于 packages/core/src/errors.ts,下面逐层解读。
二、RuntimeError 类:统一错误格式的基石
2.1 类签名与构造函数
依据 errors.api.md 的声明:
// @public
export class RuntimeError<T extends number = RuntimeErrorCode> extends Error {
constructor(code: T, message: null | false | string);
// (undocumented)
code: T;
}
其源码实现在 packages/core/src/errors.ts#L181-L188:
export class RuntimeError<T extends number = RuntimeErrorCode> extends Error {
constructor(
public code: T,
message: null | false | string,
) {
super(formatRuntimeError<T>(code, message));
}
}
几个值得注意的设计细节:
- 泛型参数:
T extends number = RuntimeErrorCode,默认约束为RuntimeErrorCode枚举,使error.code具备完整的类型提示; message的三态类型:null | false | string。在开发模式(ngDevMode开启)下传入描述性字符串;在生产构建经 tree-shaking 后message会退化为false,从而在产物中抹去描述信息,仅保留错误码;code是公开只读属性:可直接通过error.code拿到数字错误码,用于程序化判断与上报。
2.2 官方使用示例
errors.ts 的类注释给出了标准抛错范式:
throw new RuntimeError(
RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
ngDevMode && 'Injector has already been destroyed.');
2.3 仓库内的真实调用场景
RuntimeError 遍布核心运行时各模块,以下是 packages/core/src/application/application_ref.ts 中的两处代表性用法:
递归 tick 检测(application_ref.ts#L554-L562):
private tickImpl = (): void => {
(typeof ngDevMode === 'undefined' || ngDevMode) && warnIfDestroyed(this._destroyed);
if (this._runningTick) {
profiler(ProfilerEvent.ChangeDetectionEnd);
throw new RuntimeError(
RuntimeErrorCode.RECURSIVE_APPLICATION_REF_TICK,
ngDevMode && 'ApplicationRef.tick is called recursively',
);
}
// ...
};
无限变更检测保护(application_ref.ts#L603-L611):
if ((typeof ngDevMode === 'undefined' || ngDevMode) && runs >= MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while refreshing application views. ' +
'Ensure views are not calling `markForCheck` on every template execution or ' +
'that afterRender hooks always mark views for check.',
);
}
再如信号输入缺省值检查(packages/core/src/authoring/input/input_signal.ts#L137):
throw new RuntimeError(RuntimeErrorCode.REQUIRED_INPUT_NO_VALUE, message);
从这些调用可以看出统一模式:枚举错误码 + ngDevMode && 短路写法,这既是错误机制的约定用法,也是生产包体积优化的关键——开发信息随 tree-shaking 被剥离。
三、错误码格式化:NG0xxx 的生成规则
formatRuntimeErrorCode 与 formatRuntimeError 的实现位于 packages/core/src/errors.ts#L190-L215:
export function formatRuntimeErrorCode<T extends number = RuntimeErrorCode>(code: T): string {
// Error code might be a negative number, which is a special marker that instructs the logic to
// generate a link to the error details page on angular.io.
// We also prepend `0` to non-compile-time errors.
return `NG0${Math.abs(code)}`;
}
export function formatRuntimeError<T extends number = RuntimeErrorCode>(
code: T,
message: null | false | string,
): string {
const fullCode = formatRuntimeErrorCode(code);
let errorMessage = `${fullCode}${message ? ': ' + message : ''}`;
if (ngDevMode && code < 0) {
const addPeriodSeparator = !errorMessage.match(/[.,;!?\n]$/);
const separator = addPeriodSeparator ? '.' : '';
errorMessage = `${errorMessage}${separator} Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/${fullCode}`;
}
return errorMessage;
}
3.1 规则拆解
- 统一前缀
NG0:无论错误码是100还是-500,最终展示为NG0100、NG0500。0是"core 包"在整体错误码空间中的标识(编译期错误与运行时错误通过NG0区分,见源码注释 "We also prepend0to non-compile-time errors"); - 负号是"有详情页"的标记:
Math.abs(code)意味着负数仅用于编码语义,不影响展示数字。errors.ts头部注释明确说明:负号表示该错误码在 angular.io 上有专门的问题排查指南,这是为了避免在运行时额外维护一份"有指南的错误码集合"而做的编码技巧; - 详情页拼接:仅在
ngDevMode且code < 0时,错误消息末尾追加Find more at <base>/<fullCode>,例如NG0500对应.../errors/NG0500; - 标点感知:通过正则判断消息末尾是否已有句号、逗号、分号、感叹号、问号或换行,避免重复加点。
3.2 详情页基址的动态推导
ERROR_DETAILS_PAGE_BASE_URL 定义于 packages/core/src/error_details_base_url.ts,其上游 DOC_PAGE_BASE_URL 会根据当前版本动态决定域名前缀:
const full = VERSION.full;
const isPreRelease =
full.includes('-next') ||
full.includes('-rc') ||
full === '0.0.0' + '-PLACEHOLDER';
const prefix = isPreRelease ? 'next' : `v${VERSION.major}`;
return `https://${prefix}.angular.dev`;
即:预发布版本(-next / -rc)链接指向 next.angular.dev,正式版指向 v<主版本号>.angular.dev。这样错误详情页始终与开发者所用的 Angular 版本语义对齐。
四、RuntimeErrorCode 全集:按功能域划分的错误码区间
errors.api.md 的核心价值在于完整罗列了 RuntimeErrorCode 枚举。其源码 packages/core/src/errors.ts#L31-L163 按功能域分组注释,core 包保留错误码区间为 100–999,并约定各包的区间如下:
| 包 | 错误码区间 |
|---|---|
| core | 100–999 |
| forms | 1000–1999 |
| common | 2000–2999 |
| animations | 3000–3999 |
| router | 4000–4999 |
| platform-browser | 5000–5500 |
| service-worker | 5600–5699 |
| platform-server | 5700–5800 |
下面按源码分组逐一列出全部枚举项(与 golden 文件一一对应)。
4.1 变更检测(Change Detection Errors)
| 错误码常量 | 数值 |
|---|---|
EXPRESSION_CHANGED_AFTER_CHECKED |
-100 |
RECURSIVE_APPLICATION_REF_TICK |
101 |
INFINITE_CHANGE_DETECTION |
103 |
其中 RECURSIVE_APPLICATION_REF_TICK(NG0101)对应上文 tickImpl 中的递归保护,INFINITE_CHANGE_DETECTION(NG0103)对应 synchronize() 中刷新轮次超限(见 application_ref.ts#L588-L612)。
4.2 依赖注入(Dependency Injection Errors)
| 错误码常量 | 数值 |
|---|---|
CYCLIC_DI_DEPENDENCY |
-200 |
PROVIDER_NOT_FOUND |
-201 |
INVALID_FACTORY_DEPENDENCY |
202 |
MISSING_INJECTION_CONTEXT |
-203 |
INVALID_INJECTION_TOKEN |
-204 |
INJECTOR_ALREADY_DESTROYED |
-205 |
PROVIDER_IN_WRONG_CONTEXT |
-207 |
MISSING_INJECTION_TOKEN |
208 |
INVALID_MULTI_PROVIDER |
-209 |
MISSING_DOCUMENT |
210 |
INVALID_APP_ID |
211 |
INJECTOR_ALREADY_DESTROYED(NG0205)正是 RuntimeError 类注释中的示例错误码;INVALID_MULTI_PROVIDER(NG0209)在 application_ref.ts#L749-L751 的 provider 校验路径中被抛出。
4.3 模板(Template Errors)
| 错误码常量 | 数值 |
|---|---|
MULTIPLE_COMPONENTS_MATCH |
-300 |
EXPORT_NOT_FOUND |
-301 |
PIPE_NOT_FOUND |
-302 |
UNKNOWN_BINDING |
303 |
UNKNOWN_ELEMENT |
304 |
TEMPLATE_STRUCTURE_ERROR |
305 |
INVALID_EVENT_BINDING |
306 |
HOST_DIRECTIVE_UNRESOLVABLE |
307 |
HOST_DIRECTIVE_NOT_STANDALONE |
308 |
DUPLICATE_DIRECTIVE |
309 |
HOST_DIRECTIVE_COMPONENT |
310 |
HOST_DIRECTIVE_UNDEFINED_BINDING |
311 |
HOST_DIRECTIVE_CONFLICTING_ALIAS |
312 |
MULTIPLE_MATCHING_PIPES |
313 |
UNINITIALIZED_LET_ACCESS |
314 |
NO_BINDING_TARGET |
315 |
INVALID_BINDING_TARGET |
316 |
INVALID_SET_INPUT_CALL |
317 |
INVALID_STYLE_PROP_VALUE |
-318 |
从数值序列可看出模板错误是当前占用码位最多、增长最快的分组,其中 300–318 区间还密集覆盖了宿主指令(host directive)各类非法组合。
4.4 应用启动(Bootstrap Errors)
| 错误码常量 | 数值 |
|---|---|
MULTIPLE_PLATFORMS |
400 |
PLATFORM_NOT_FOUND |
-401 |
MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP |
402 |
BOOTSTRAP_COMPONENTS_NOT_FOUND |
-403 |
PLATFORM_ALREADY_DESTROYED |
404 |
ASYNC_INITIALIZERS_STILL_RUNNING |
405 |
APPLICATION_REF_ALREADY_DESTROYED |
406 |
RENDERER_NOT_FOUND |
407 |
PROVIDED_BOTH_ZONE_AND_ZONELESS |
408 |
ASYNC_INITIALIZERS_STILL_RUNNING(NG0405)与 APPLICATION_REF_ALREADY_DESTROYED(NG0406)均在 application_ref.ts 中被抛出(分别见第 482 行与第 799-800 行);PROVIDED_BOTH_ZONE_AND_ZONELESS(NG0408)则对应 Zone.js 与 zoneless 混用的配置冲突检测。
4.5 水合(Hydration Errors)
| 错误码常量 | 数值 |
|---|---|
HYDRATION_NODE_MISMATCH |
-500 |
HYDRATION_MISSING_SIBLINGS |
-501 |
HYDRATION_MISSING_NODE |
-502 |
UNSUPPORTED_PROJECTION_DOM_NODES |
-503 |
INVALID_SKIP_HYDRATION_HOST |
-504 |
MISSING_HYDRATION_ANNOTATIONS |
-505 |
HYDRATION_STABLE_TIMEDOUT |
-506 |
MISSING_SSR_CONTENT_INTEGRITY_MARKER |
-507 |
MISCONFIGURED_INCREMENTAL_HYDRATION |
508 |
HYDRATION_MISSING_NODE_ON_PATH |
509 |
PARENT_NODE_NOT_FOUND |
510 |
这是 SSR 场景最常见的错误家族,全部集中在 500–510 区间。水合实现位于 packages/core/src/hydration,其中 hydrations/utils.ts 与 hydrations/api.ts 均直接引用了 formatRuntimeError 来格式化这些错误。
4.6 信号(Signal Errors)
| 错误码常量 | 数值 |
|---|---|
SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT |
600 |
REQUIRE_SYNC_WITHOUT_SYNC_EMIT |
601 |
ASSERTION_NOT_INSIDE_REACTIVE_CONTEXT |
-602 |
SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT(NG0600)的抛出点在 application_ref.ts#L83,用于禁止在非法的响应式上下文(如模板执行期外)中写信号。
4.7 动画、i18n、Defer 与 Standalone
| 分组 | 错误码常量 | 数值 |
|---|---|---|
| 动画 | ANIMATE_INVALID_VALUE |
650 |
| i18n | INVALID_I18N_STRUCTURE |
700 |
| i18n | MISSING_LOCALE_DATA |
701 |
| Defer(750–799 区间) | DEFER_LOADING_FAILED |
-750 |
| Defer | DEFER_IN_HMR_MODE |
-751 |
| Standalone | IMPORT_PROVIDERS_FROM_STANDALONE |
800 |
DEFER_LOADING_FAILED(NG0750)在 packages/core/src/defer/instructions.ts 的 @defer 块加载失败路径中抛出。
4.8 JIT 编译与运行时杂项(900–923)
| 错误码常量 | 数值 |
|---|---|
INVALID_DIFFER_INPUT |
900 |
NO_SUPPORTING_DIFFER_FACTORY |
901 |
VIEW_ALREADY_ATTACHED |
902 |
INVALID_INHERITANCE |
903 |
UNSAFE_VALUE_IN_RESOURCE_URL |
904 |
UNSAFE_VALUE_IN_SCRIPT |
905 |
MISSING_GENERATED_DEF |
906 |
TYPE_IS_NOT_STANDALONE |
907 |
MISSING_ZONEJS |
908 |
UNEXPECTED_ZONE_STATE |
909 |
UNSAFE_ATTRIBUTE_BINDING |
-910 |
VIEW_ALREADY_DESTROYED |
911 |
COMPONENT_ID_COLLISION |
-912 |
IMAGE_PERFORMANCE_WARNING |
-913 |
UNEXPECTED_ZONEJS_PRESENT_IN_ZONELESS_MODE |
914 |
MISSING_NG_MODULE_DEFINITION |
915 |
MISSING_DIRECTIVE_DEFINITION |
916 |
EXTERNAL_RESOURCE_LOADING_FAILED |
918 |
DEF_TYPE_UNDEFINED |
-919 |
NG_MODULE_ID_NOT_FOUND |
920 |
DUPLICATE_NG_MODULE_ID |
921 |
VIEW_DESTROYED_INSERT_ERROR |
922 |
VIEW_DESTROYED_MOVE_ERROR |
923 |
注意码位 917 已被移除(源码中以 /* 917 - Removed */ 注释保留占位,避免历史错误码复用造成的歧义)。安全相关错误 UNSAFE_VALUE_IN_RESOURCE_URL(NG0904)、UNSAFE_VALUE_IN_SCRIPT(NG0905)、UNSAFE_ATTRIBUTE_BINDING(NG0910)构成 XSS 防护体系的一环,对应文档基址常量旁的 XSS_SECURITY_URL(见 error_details_base_url.ts#L35-L36)。
4.9 信号集成、Output 与 Repeater(950–956)
| 分组 | 错误码常量 | 数值 |
|---|---|---|
| 信号集成 | REQUIRED_INPUT_NO_VALUE |
-950 |
| 信号集成 | REQUIRED_QUERY_NO_VALUE |
-951 |
| 信号集成 | REQUIRED_MODEL_NO_VALUE |
952 |
| Output | OUTPUT_REF_DESTROYED |
953 |
| Repeater | LOOP_TRACK_DUPLICATE_KEYS |
-955 |
| Repeater | LOOP_TRACK_RECREATE |
-956 |
REQUIRED_INPUT_NO_VALUE(NG0950)由 input.required() 缺失绑定触发,抛出点在 input_signal.ts#L137;LOOP_TRACK_DUPLICATE_KEYS(NG0955)与 LOOP_TRACK_RECREATE(NG0956)由 @for 循环的 track 表达式冲突触发,实现在 packages/core/src/render3/list_reconciliation.ts。
4.10 运行时依赖追踪与 resource() API(980–992)
| 分组 | 错误码常量 | 数值 |
|---|---|---|
| 运行时依赖追踪 | RUNTIME_DEPS_INVALID_IMPORTED_TYPE |
980 |
| 运行时依赖追踪 | RUNTIME_DEPS_ORPHAN_COMPONENT |
981 |
| resource() | MUST_PROVIDE_STREAM_OPTION |
990 |
| resource() | RESOURCE_COMPLETED_BEFORE_PRODUCING_VALUE |
-991 |
| resource() | INVALID_RESOURCE_CREATION_IN_PARAMS |
992 |
这一区间是最新加入的,对应新版 resource() API 与运行时依赖追踪(runtime dependency tracker)功能。至此 core 包错误码用满至 999 上限(源码注释 "Upper bounds for core runtime errors is 999")。
五、错误码语义速查:正负号与区间解读指南
5.1 负号 = 有官方错误指南
这是理解 Angular 错误输出的最重要规则。errors.ts 头部注释(packages/core/src/errors.ts#L11-L30)明确说明:
the minus sign denotes the fact that a particular code has a detailed guide on angular.io.
因此当你在开发模式看到类似 NG0500: ... Find more at .../errors/NG0500 时,说明该错误有官方深度排查文档;反之,正数错误码(如 NG0101、NG0208)通常只有代码内消息,没有独立详情页。
5.2 功能域优先定位法
排查线上错误时,可按区间快速缩小范围:
NG0-1xx变更检测;NG0-2xx依赖注入;NG0-3xx模板与宿主指令;NG0-4xx应用启动与平台生命周期;NG0-5xxSSR 水合;NG0-6xx信号与动画;NG0-7xxi18n 与@defer;NG0-8xxstandalone;NG0-9xxJIT、安全、信号集成、@for与resource()。
再结合 errors.api.md 中的常量名,即可在仓库内全局搜索对应抛错点。
六、如何在业务代码中复用这套机制
虽然 RuntimeError 主要服务于框架内部,但理解其设计对编写可维护的库与业务代码同样有借鉴意义:
- 用枚举管理错误码:所有错误码集中定义、按域分组、预留区间,避免魔法数字;
- 负号编码"附加信息":把"是否有详情文档"这类元信息编码进错误码本身,省去运行时维护额外集合的成本;
ngDevMode && message写法:开发时保留完整描述,生产构建自动剥离,兼顾可调试性与包体积;- 统一格式化入口:通过
formatRuntimeError保证所有错误输出格式一致(NG0xxx: message),并自动附带文档链接。
若需在框架外部判断错误类型,可读取 RuntimeError 实例的 code 属性,例如:
try {
// 触发变更检测相关操作
} catch (error) {
if (error instanceof RuntimeError) {
switch (error.code) {
case RuntimeErrorCode.INFINITE_CHANGE_DETECTION:
// 处理无限变更检测
break;
// ...
}
}
}
七、延伸阅读路径
- goldens/public-api/core/errors.api.md:本文依据的公开 API 报告(错误码权威清单);
- packages/core/src/errors.ts:
RuntimeError、formatRuntimeError、formatRuntimeErrorCode与RuntimeErrorCode的完整实现及分组注释; - packages/core/src/error_details_base_url.ts:错误详情页基址与 XSS 文档地址的动态推导逻辑;
- packages/core/src/application/application_ref.ts:
RECURSIVE_APPLICATION_REF_TICK、INFINITE_CHANGE_DETECTION等错误码的真实抛错场景; - packages/core/src/authoring/input/input_signal.ts:
REQUIRED_INPUT_NO_VALUE的触发实现; - packages/core/src/render3/list_reconciliation.ts:
@for循环 track 冲突错误码(NG0955/NG0956)的实现位置。
掌握这份错误码索引,就等于拿到了一张 Angular Core 运行时"故障地图":看到控制台中的 NG0xxx,即可从本文的区间表、符号名直达对应源码,快速定位问题根因。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
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