orion-error 0.8.2

Structured error governance for layered Rust systems
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
# Wukong 错误治理模型:稳定契约、可靠诊断与适配输出

这篇文章讨论一个问题:工业级系统如何把千变万化的错误纳入可治理、可诊断、可演进的结构。

如果只想快速把握主线,可以先读这四段:

1. [核心矛盾](#核心矛盾):为什么错误治理的本质是”收敛 vs. 诊断”。
2. [我们的方案:Wukong 错误治理模型](#我们的方案wukong-错误治理模型):模型如何用稳定契约、可靠诊断和适配输出治理错误。
3. [基于 Rust 的错误治理方案](#基于-rust-的错误治理方案):如何用 `orion-error` 落地。
4. [工业级应用验证:WarpParse](#工业级应用验证warpparse):高吞吐 ETL 场景如何验证这套方法。

本文分三层结构,可按需阅读:

- **方法论层**([错误处理是原型与工业级的分水岭](#错误处理是原型与工业级的分水岭) → [治理等级](#治理等级)):核心矛盾、Wukong 模型、五项原则、三种传播模式、治理成熟度。
- **工程落地层**([基于 Rust 的错误治理方案](#基于-rust-的错误治理方案)):`orion-error` 如何把方法论落实到 Rust 代码,含设计规则和测试规范。
- **工业验证层**([工业级应用验证:WarpParse](#工业级应用验证warpparse) → [面向 AI 的工程化复用](#面向-ai-的工程化复用)):WarpParse 高吞吐 ETL 场景验证,以及如何沉淀为 AI 可复用的 engineering skills。
- **附录**([语言机制与生态采纳](#附录语言机制与生态采纳)):Java / TypeScript / Go / C++ / Swift / C# 的落地对比,不读不影响理解主线。

---

## 错误处理是原型与工业级的分水岭

原型只需证明正确路径可以跑通;工业级应用还须在非理想条件下可运行、可诊断、可恢复、可演进。

系统不会长期运行在理想条件下。输入变化,依赖退化,网络抖动,配置漂移,数据积累脏状态,业务规则迭代。处理路径随用户、环境、状态和策略动态分叉。正确路径不是系统运行的全部;失败、降级、重试、回滚、补偿和人工介入同样是生命周期的一部分。

因此,错误不是"正常逻辑之外的意外文本",而是系统在非理想条件下继续运行、恢复状态、决定对外响应、支持诊断时必须传递的信息。

很多项目在早期把错误处理当作"每个函数自己的事"。每个函数决定如何表达失败,然后这个决策在下一个函数、下一个模块、下一个边界被重新做一次。

这种模式在小型项目中可以工作——调用链短、边界少、参与者对上下文有共同记忆。但当错误信息没有统一形态,且开始跨越团队、子系统、服务边界、协议边界或长期兼容边界时,失败路径就会变得不可治理:

- 同一种失败,A 模块返回字符串,B 模块返回 enum,C 模块直接 panic
- 边界输出时,每一层都在重新拼 JSON,结构却不一致
- 排障时,日志中有散落的消息,却没有一条完整的错误路径
- 重构时,不敢动错误类型——因为不知道哪些上层依赖了错误字符串内容

这些现象不一定来自某个函数"写得不好"。它们也可能来自缺乏统一工具、团队规范缺失、历史演进、人员流动或边界职责不清。关键在于:一旦错误需要在多个边界之间传播、被多个角色消费、并长期保持兼容,错误就不再只是局部控制流。

错误治理定义了失败发生后系统如何保留信息、跨层传递、对外暴露、支撑诊断和演进。它不是业务逻辑的装饰,而是业务逻辑失败时的信息架构,也是系统对抗腐化与脆弱的骨架。

---

## 行业一直在探索错误处理

错误处理的重要性已得到专业工程师和工业界的共同认可。真正困难的不是"要不要处理错误",而是如何有效处理:既不能让它吞没业务代码,也不能让失败路径退化成不可治理的字符串;既要让调用方做稳定决策,又要让排障方保留足够细节。

不同语言对错误处理的设计,正说明这个问题长期没有唯一答案。

- C 主要依赖返回码、`errno` 和约定。直接、低成本,但错误信息容易分散,调用方也容易漏检。
- Java 把异常机制作为主路径,区分 checked exception 与 unchecked exception。它强化了错误传播能力,但也带来异常层次膨胀、边界语义不清和过度捕获的问题。
- Go 强调显式返回 `error`,让失败路径可见。但如果缺少团队约束,错误很容易变成层层包装的字符串。
- Rust 通过 `Result<T, E>`、`?`、枚举和类型系统把错误纳入普通控制流,让错误路径显式可组合。但分类、上下文、边界暴露和诊断策略仍需工程层面设计。

这些设计各有取舍。语言机制能降低错误处理成本,却不能替代错误治理本身。只能说,在错误处理这件事上,各种语言都还在探索基础能力:异常、返回值、`Result`、错误码,各有取舍,也都没有彻底解决问题。可从系统构建者的角度看,我们真正关心的并不是语法层面怎么抛、怎么传,而是失败信息在系统中如何被分类、保留、转换、暴露和观测。

对研发团队而言,错误处理横跨类型设计、调用链传播、日志与观测、协议输出、用户体验、运维策略和长期兼容。任一环节各自为政,最终都会在排障、重构和边界协作时暴露成本。

因此,错误处理不能只依赖个人经验和局部习惯,而需要一套可讨论、可执行、可演进的方法论。

到今天为止,业界并没有形成跨语言、跨框架、跨业务形态的统一错误治理方案。但一些优秀项目已经在不同方向给出了可参考实践:

- 稳定错误码
- 结构化诊断
- 集中边界策略
- 状态化错误呈现
- 可观测错误信号
- 面向用户的修复提示

这些实践说明,错误治理不是单一 API 问题,而是一组围绕失败信息展开的工程约束。

## 来自优秀项目的证据

| 项目 | 做法 | 启发 |
|------|------|------|
| gRPC | 跨语言 RPC 失败收敛为标准状态码 | 稳定分类让调用方可重试、降级、告警和映射用户响应 |
| PostgreSQL | 使用稳定 SQLSTATE 错误码,不依赖错误文本 | 机器契约和人类文案应该分离 |
| Kubernetes | 就绪状态、失败原因和 condition 写入 `status` | 错误可以是可查询、可自动化处理的系统状态 |
| Terraform | diagnostics 含 severity、summary、detail、attribute path | 错误应指出位置、原因和修复方向 |
| rustc | 错误码、源码位置、label、note、help 构成诊断体验 | 诊断信息本身是产品体验 |
| Envoy | access log response flags 表达稳定失败原因 | 边界层错误应能被聚合、搜索、告警和自动分析 |

这些项目形态不同,方向一致:优秀的错误处理都把失败路径设计成稳定的信息系统——既有机器可判断的分类,也有人能理解的诊断;既在内部保留细节,也在边界按策略暴露;既服务当前请求,也服务后续排障、监控和演进。

---

## 核心矛盾

任何错误治理方案都要处理一对根本矛盾:

**收敛 vs. 诊断**

- 调用方需要**稳定的、有限的分类**,否则无法做出治理决策(重试、降级、告警、返回给用户)
- 排障方需要**完整的、保留细节的信息**,否则无法定位根因

这两类需求都合理,但天然拉向不同方向。

如果错误向调用方暴露过多技术细节,上层就会依赖数据库、网络库、文件系统、第三方 SDK 的具体失败形态,系统边界被技术实现穿透。重构底层实现时,错误契约被迫变化。

如果错误只保留上层业务分类,排障时又会失去关键路径:原始失败是什么、发生在哪个组件、经过了哪些层、每层追加了什么上下文、最终为何被映射成这个对外响应。

因此,错误治理的主要矛盾不是"要不要包装错误",而是:**如何同时让错误在治理层面收敛,在诊断层面保真。**

### 不充分的解决方案

| 策略 | 对调用方 | 对排障方 |
|------|---------|---------|
| 只抛技术异常 | 无法治理 | 信息完整 |
| 只抛业务错误 | 可以治理 | 丢失根因 |
| 纯字符串链式包装 | 无法治理 | 可读但不可结构化查询 |
| 保留类型信息的链式包装 | 可做局部治理 | 保留原因链,但分类与边界策略仍需额外约束 |
| 吞掉错误 | 干净 | 丢失所有信息 |

纯字符串链只是把错误文案一层层拼起来;保留类型信息的链式包装(cause chain、typed wrapping、`errors.Is`/`errors.As`)可支持一定程度的结构化查询,但它只解决"原因如何保留和查询",不自动解决稳定错误标识、分类边界、暴露策略和治理动作映射。

这些方案如果只依赖单一形态同时满足两类需求,结果往往是牺牲其中一边:要么调用方信息太散无法自动化治理,要么排障方信息太少只能翻日志和复现。

接下来的问题是:现有语言机制和常见实践,为什么还没有把这件事真正解决?

### 砖块不等于建筑

一个常见质疑:Java 的异常 + 错误码、Go 的 sentinel error + wrapping、Rust 的 enum + cause chain,不是已经覆盖了 Wukong 错误治理模型要解决的大部分问题吗?

这些机制都是砖块,但砖块不等于建筑。问题出在三点。

**错误标识不稳定。** Java 生态中异常类型承担分发角色:`catch (OrderNotFoundException e)`。但异常类型受继承层次控制,重构时会变。错误码是异常的附赠字段,调用方先 `instanceof` 再取 `getErrorCode()`——错误码不是路由主键。Wukong 错误治理模型的主张是:把错误标识提升为契约通道的第一公民,边界策略基于错误标识路由,与继承层次解耦。不管用 `enum`、错误码字符串还是 tagged union 表达,错误标识本身必须是稳定的、可文档化的、被测试约束的契约。

**分类空间天然膨胀。** 异常机制鼓励”一种失败一个类”:`SubmitDependencyUnavailableException`、`InvalidStateException`……类数量跟随业务失败模式无限制增长,没有机制强制收敛到有限分类。如果异常类型同时承担分类职责,分类就不可能稳定——每新增一个 exception class,所有依赖异常层次做路由的边界都可能受影响。Wukong 错误治理模型把分类空间(`R`)限制为有限枚举,新增变体是有意为之的兼容演进,不是随意加类。

**契约和诊断共用同一通道,互相牵制。** cause chain、structured wrapping、`errors.Is`/`errors.As` 解决的是诊断保留——根因如何保留和查询。它们不解决:该触发重试还是降级?该映射到哪个 HTTP 状态码?该暴露给用户还是只记日志?这些治理动作若由异常类型、字段或局部判断来做,就会散落在每个 handler、每个 catch 块、每个 `errors.Is` 调用中。Wukong 错误治理模型把契约信息和诊断信息分离成不同通道,治理动作由集中策略而非局部代码决定。

综上,关键区别不在于你用什么语言机制,而在于你的错误架构有没有这四根承重墙:稳定错误标识(不受类型重构影响)、有限分类空间(受兼容演进规则约束)、诊断保留(跨层不丢失)、集中边界策略(不在 handler 中重复决策)。失去这些结构,任何语言的错误处理都会长成不可治理的灌木丛——哪怕砖块是顶好的。

### 我们的方案:Wukong 错误治理模型

本文把这套方法称为 **Wukong 错误治理模型**。它要”降”的不是神话里的妖怪,而是工业级系统里千变万化的错误:用稳定契约让错误现形,用可靠诊断保留根因和上下文,用适配输出为不同接收端提供稳定、合适的信息,并通过生产观测持续演进。

之所以叫 Wukong,是借用悟空在西行路上一路降妖除魔的意象。放到软件工程里,我们要降伏、封印的不是妖魔,而是各种错误:让它们有名有类、可查可控,不再以散乱字符串、隐式包装和边界泄露的形式四处作乱。

核心方法论:**把稳定契约和可靠诊断分离到两个维度,再按接收端生成适配输出。**

```text
错误内部模型 = 契约通道 + 诊断通道
适配输出 = 对内部模型按策略生成的不同输出视图
```

契约通道由稳定错误标识、稳定分类和策略语义组成,服务治理决策(重试、降级、告警、HTTP/RPC/CLI 映射、用户提示、SLA 统计),应有限、稳定、可文档化、可测试。

诊断通道由诊断链、上下文和细节组成,服务问题定位(底层原因、经过的层、当前操作、关键字段、组件、环境),可以更丰富、更动态,但不应成为外部调用方的稳定契约。

契约通道和诊断通道描述的是错误在系统内部携带的信息结构;HTTP response、RPC error、CLI output、log record、metric label 和 debug report 则是到达不同边界后生成的适配输出。输出视图不应反过来污染错误内部模型。

| 通道 | 包含什么 | 服务谁 | 稳定性要求 |
|------|---------|--------|------------|
| 契约通道 | 稳定错误标识、稳定分类、category、retryable、暴露等级 | 调用方、网关、监控、运维策略、协议客户端 | 高,应被文档化和测试约束 |
| 诊断通道 | 原因链、操作上下文、关键字段、动态细节、底层错误 | 开发者、SRE、排障工具、日志系统 | 可动态变化,但须详实可靠且可追溯 |

`category` 是稳定分类的固定治理维度,例如业务、系统、配置和逻辑错误,用于快速区分错误归属域,辅助告警路由、日志聚合和边界策略。`retryable`、暴露等级、HTTP 状态码、日志级别等不是错误文本本身,而是由稳定错误标识、分类和环境策略派生的边界决策。

这里有四个概念需要严格区分:

- `identity` 是长期稳定的错误标识,面向协议、监控、告警、文档和兼容性。
- `reason` 是代码中的领域分类表达,通常是 enum、sealed class、tagged union 或异常子类。
- `category` 是粗粒度治理维度,用于快速判断归属域和路由策略。
- `policy` 是边界决策规则,由 identity、category 和环境策略共同决定输出视图。

简言之:identity 是契约主键,reason 是代码表达,category 是治理维度,policy 是边界动作。不要用错误消息替代 identity,不要用 category 替代 identity,也不要把 policy 决策散落到各个 handler 中。

`reason` 与 `identity` 的关系需要更严格地理解:`reason` 是进程内代码用来表达领域分类的类型,`identity` 是跨边界、跨版本的错误标识。一个 reason 变体通常提供一个稳定 identity;外部协议、监控和告警应依赖 `identity.code`,不应依赖 Rust enum 名、Java class 名或 `Display` 文案。跨语义域传播时,上层不应暴露下层 reason 类型,但可以把下层错误保留在诊断链中。

| 组成部分 | 含义 | 例子 |
|----------|------|------|
| 稳定错误标识 | 机器可判读的错误主键,面向长期兼容 | `order.not_found`、`system.timeout` |
| 稳定分类 | 面向治理决策的有限类别 | 业务错误、配置错误、系统错误、超时、限流 |
| 治理属性 | 从稳定错误标识和分类派生的辅助决策字段 | category、retryable、暴露等级、HTTP 状态码 |
| 诊断链 | 跨层传播时保留的 cause/source 路径 | service failure -> repository failure -> database timeout |
| 上下文 | 当前操作的结构化环境,回答”在哪、对谁、执行什么” | operation、tenant、path、order_id、component |
| 细节 | 当前层对这次失败的具体解释 | `read config failed`、`upstream returned 503` |

同一个错误通过不同视图同时服务两类需求,减少调用方和排障方互相牺牲。

契约通道应保持低基数、低动态性和长期稳定。它不应包含租户 ID、文件路径、SQL、HTTP body、第三方错误文本、用户输入或具体字段值;这些信息属于诊断通道,应进入 detail、context 或 source chain。契约通道越稳定,监控聚合、告警路由、协议兼容和自动化决策越可靠;诊断通道越详实可靠,排障和修复越有效。

不同信息的稳定性不同,设计和测试时不应一视同仁:

| 信息 | 稳定性 | 是否适合作为外部契约 |
|------|--------|----------------------|
| 错误标识码 | 最高 | 是 |
| category | 高 | 通常是 |
| reason 变体 | 中高 | 代码内契约 |
| retryable / visibility / HTTP 状态码 | 中 | 策略契约 |
| detail | 低 | 否 |
| context fields | 低到中 | 通常否 |
| source chain | 低 | 否 |

因此,测试应优先断言 identity 和策略结果,而不是断言 detail 文案;文档应承诺稳定错误标识和分类语义,而不是承诺具体诊断文本。

---

## 方案原则

Wukong 错误治理模型落地需要五项原则配合:统一载体承载结构,契约通道保持稳定,诊断通道详实可靠,边界集中输出,外部生态显式桥接。

本节代码只表达方法论形状,是语言无关的伪代码。

### 原则一:用统一载体承载契约与诊断信息

**自有跨层错误传播路径应使用统一的结构模型。**

反例:

```rust
// A 模块返回 io::Error
fn read_file() -> io::Result<Data>

// B 模块返回自定义 enum
fn validate() -> Result<Data, ValidationError>

// C 模块返回字符串
fn process() -> Result<Data, String>
```

每条错误路径的调用者都需学习一套新形状。组合两个不同函数的错误路径时,调用方既要判断分类,又要重新拼接诊断信息,还要决定边界输出格式。

正例:

```text
read_file() -> Result<Data, StructuredError<ErrorClass>>
validate() -> Result<Data, StructuredError<ErrorClass>>
process() -> Result<Data, StructuredError<ErrorClass>>
```

载体模型统一,才能同时承载稳定分类和诊断信息。变化的是分类空间和上下文。不同层可拥有自己的分类空间,但跨层传播时应有清晰的收敛或边界转换规则。

统一载体不是要求第三方库、标准库、框架异常或协议错误全改成同一种类型;它要求团队控制的内部传播路径使用同一种结构模型,进入或离开外部生态时显式桥接。

### 原则二:让契约通道保持稳定

**错误分类契约应按向后兼容规则演进。**

错误分类体系是契约——调用方依赖它做治理决策。"稳定"不是指不能新增分类,而是已有分类的机器标识和语义不能随意变化。

错误标识是契约通道里的机器主键,通常表现为稳定字符串、错误码或协议字段(如 `business.not_found`、`system.timeout`)。调用方、网关、监控、告警和文档都应依赖这个标识,而非错误消息文本。

分类契约的兼容规则:

- 可以新增错误标识或分类,表达新的业务失败或系统失败。
- 不应删除已对外承诺的错误标识;若必须废弃,应保留兼容映射或经明确的版本迁移。
- 不应改变已有标识的语义(如把 `business.not_found` 从"资源不存在"改成"无权限访问")。
- 不应让同一标识在不同边界产生矛盾的治理动作(如一处可重试、另一处不可重试)。
- 可以调整错误文案、诊断细节、上下文字段和底层原因链,只要不破坏标识和分类语义。

| 应该稳定的 | 可以变化的 |
|-----------|-----------|
| 稳定错误标识 | 诊断细节 |
| 分类语义 | 错误信息文案 |
| category(业务/系统/配置) | 具体的技术细节 |

稳定分类的另一个好处:它是人和系统之间的共享接口。运维配置告警规则、网关配置状态码映射、API 文档描述错误响应——全部依赖稳定错误标识和分类语义,而非错误文本。枚举、异常类型、错误码或 tagged union 只是表达这个契约的具体方式。

分类粒度需要被约束。新增稳定错误标识通常需要满足至少一个条件:

- 调用方需要不同的治理动作,如重试、降级、停止重试或人工介入。
- 边界需要不同的协议状态、公开错误码、用户消息或修复提示。
- 监控、告警、SLA 或运营报表需要独立聚合。
- SRE、业务方或规则开发者需要把它作为独立失败类型跟踪。
- 语义长期稳定,不依赖当前数据库、SDK、网络库或实现细节。

以下情况通常不应新增稳定错误标识:

- 只是错误文案不同。
- 只是字段名、文件名、租户、路径、行列号或样本内容不同。
- 只是底层库错误类型不同,但治理动作相同。
- 只是为了让日志更详细。

这些动态差异应进入诊断通道。否则分类空间会不断膨胀,契约通道从稳定契约退化成另一种日志文本。

### 原则三:让诊断通道跨层保真

**错误在内部传播时应追加信息,不应破坏已有诊断链。**

反例:

```text
repository() -> Result<Data, RepoError> {
    // 数据库连接失败,返回 RepositoryConnectionFailed
}

service() -> Result<Data, ServiceError> {
    data = repository()?  // 丢弃了下层错误的具体信息
    return data
}
```

正例:

```text
repository() -> Result<Data, StructuredError<RepositoryClass>> {
    // 数据库连接失败,保留原始数据库错误
}

service() -> Result<Data, StructuredError<ServiceClass>> {
    data = repository()
        .source_err(ServiceDependencyFailed, "load repository data failed")
    return data
}
```

每层保留的信息形成完整错误链,排障时从最终错误追溯到原始根因。

这里有两种操作:

- 若当前层只把下层分类收敛到上层分类空间、不建立新语义边界,应保留原有诊断链,不制造新错误叙事。
- 若当前层要表达新的失败语义,应把下层错误作为原因保留,并追加当前层解释。

判断标准是语义域,不是函数层数。

若上下层属同一语义域,错误转换通常只是分类收敛。例如 database driver、query executor、repository helper 同属 data access 语义域;它们之间可将底层连接失败收敛为 `RepositoryConnectionFailed`,同时保留原始数据库错误和上下文,不必每层都追加业务叙事。

若错误跨越了语义域或架构责任边界,就应建立新语义边界。例如 data access 失败进入 order service 时,上层关心的不是"数据库连接失败",而是"订单草稿加载失败"或"提交订单依赖不可用"。此时应追加 service 层语义,并把下层 data access 错误作为原因保留。

辅助判断问题:

- 当前层是否在向上层隐藏实现细节?
- 当前层是否拥有新的业务含义、用户意图或操作目标?
- 当前层是否会改变治理动作(如从底层 timeout 映射为业务依赖不可用)?
- 若未来替换下层实现,上层错误契约是否应保持不变?

若答案为"是",这里通常是语义边界;若只是模块拆分、工具函数或同领域内的技术分层,只需分类收敛和诊断保留。边界输出时再按策略做脱敏和格式化。

### 原则四:在边界集中输出

**边界暴露策略应集中定义,不在每个边界点重新决定。**

结构化错误在内部传播时携带契约通道和诊断通道;到达边界时,边界层从契约通道取得稳定错误标识(`error_identity`),交给统一策略生成输出视图。

```text
StructuredError<ErrorClass>
    -> error_identity
    -> exposure_policy
    -> HTTP response / RPC error / CLI output / log record / metric label
```

反例:

```text
// handler A
match err {
    NotFound => HttpResponse(404, "not found"),
    Timeout => HttpResponse(503, "try again"),
}

// handler B
match err {
    NotFound => HttpResponse(404, "resource missing"),
    Timeout => HttpResponse(504, "gateway timeout"),
}
```

两个 handler 对同一错误的输出不一致。

正例:

```text
// 策略集中定义
policy.status(error_identity) {
    match error_identity {
        "business.not_found" => 404
        "system.timeout" => 503
        _ => 500
    }
}

// 所有边界点使用同一策略
render_error_response(err, policy)
```

集中策略不只负责 HTTP 状态码,还应覆盖不同输出视图:

- 对外错误码和用户可见消息
- HTTP/RPC/CLI 格式映射
- 日志级别和结构化日志字段
- 是否触发告警或计入 SLA
- 是否建议调用方重试、降级或停止重试
- 诊断信息脱敏和暴露等级
- 指标标签和错误聚合维度

这些决策若散落在每个 handler、worker、controller 中,同一错误标识就可能在不同边界产生矛盾输出,破坏契约通道稳定性。

### 原则五:显式桥接外部生态

**进入外部生态(日志系统、标准错误接口、第三方库)应是显式的。**

反例:

```text
// 调用者在不知情的情况下把错误降级为普通字符串
handle(error_as_text)  // 擦除了结构化信息
```

正例:

```text
// 显式选择进入外部生态
plain_error = err.to_plain_error()
log_record = err.to_log_record(redaction_policy)
```

显式桥接确保结构化信息的丢失、脱敏或降级是有意为之而非无意遗漏。每个桥接函数应有清楚的桥接契约:

- 目标消费者是谁:用户、协议客户端、日志系统、监控系统、第三方库,还是标准错误接口。
- 保留什么:稳定错误标识、分类、原因链摘要、操作上下文、关键字段、retryable、visibility。
- 丢弃什么:内部实现类型、敏感字段、过长底层错误、无法稳定解析的动态文本。
- 脱敏什么:token、密钥、用户隐私、租户隔离信息、内部拓扑、SQL 片段或请求载荷。
- 如何降级:当目标生态只能接收字符串或普通异常时,哪些字段压缩进文本,哪些彻底丢失。

不同桥接目标应有不同契约:写日志时保留错误标识、分类、上下文、关键字段和原因链摘要;对外响应时只暴露错误码、可公开消息和修复提示;进入标准错误接口时可能只保留文本和 source 链。桥接的重点不是"所有信息都带出去",而是每次输出都可审计、可测试、可预期。

### 边界安全与跨进程输出

结构化错误在内部保留了更多信息,因此边界输出必须同时处理治理、安全和隐私。诊断通道可以丰富,但不代表所有信息都能离开当前信任域。

信任域应分层处理:

| 信任域 | 推荐传递内容 |
|--------|--------------|
| 进程内 | 完整结构化错误、source chain、detail、context |
| 服务间 / 进程间 | 协议快照、identity、category、公开消息、retryable、correlation id |
| 用户边界 | 稳定错误码、可公开消息、必要修复提示、关联 ID |
| 观测系统 | 脱敏后的诊断摘要、关键 context、聚合标签 |
| 支持包 / 调试报告 | 权限控制下的更完整诊断信息,并受脱敏和生命周期约束 |

不同视图应有不同暴露等级:

- 用户响应:只包含稳定错误码、可公开消息、必要的修复提示和关联 ID。
- 协议客户端:包含机器可判断的 identity、category、retryable 等治理字段,但不暴露内部 source chain。
- 日志和 report:保留脱敏后的上下文、原因链摘要和关键诊断字段。
- 支持包或调试报告:可包含更完整诊断信息,但必须经过权限、脱敏和生命周期控制。

跨服务、跨进程或跨消息队列传播时,通常不应直接传递完整内部错误对象。更稳妥的方式是传递协议快照:稳定 identity、category、公开消息、retryable、correlation id 或 trace id;完整诊断链留在产生错误的服务内,通过日志、trace 和 report 关联查询。

这样可以避免两个问题:一是把内部实现细节泄露给外部调用方;二是让下游依赖上游的内部错误类型、source chain 或底层库形态。跨进程边界上的错误契约应是协议契约,不是进程内诊断结构的直接序列化。

---

## 错误传播的三种模式

以上五项原则定义了错误治理的**静态结构**:统一载体承载信息,契约通道保持稳定,诊断通道跨层保真,边界集中输出,外部生态显式桥接。下面三种传播模式描述的是错误的**动态生命周期**——一个错误如何首次进入结构化体系、如何跨语义域转换、如何最终在边界输出。原则回答"结构应该长什么样",模式回答"运行时如何流转";两者是同一套方法论的互补视角。

错误传播不是机械向上抛。一个工业级错误经历三种动作:首次进入、跨层转换、边界输出。

### 首次进入

原始错误(IO、解析、网络错误)首次进入结构化系统,需同时完成:

1. 选择分类(业务 vs 系统 vs 配置)
2. 给出当前层解释(detail)
3. 保留原始错误作为底层原因

三个诊断概念的分工:`source/cause` 回答根因是什么,`context` 回答在哪、对谁、执行什么,`detail` 回答当前层如何理解这次失败。

### 跨层转换

上层将下层错误分类收敛到自己的分类空间:若只做分类重新映射,保留所有诊断信息;若要建立新语义边界,将下层错误作为原因包裹。取决于当前层是否是新语义边界。

语义边界不是架构层边界。跨函数、跨文件、跨 helper、跨 adapter,不必然意味着要建立新的错误叙事;跨越业务意图、用户操作、治理动作或实现隐藏边界时,才需要新的稳定错误标识。过度包装会让 source chain 充满重复的 "failed to process";包装不足又会把底层技术细节暴露给上层契约。判断标准始终是语义责任,而不是调用栈深度。

跨层传播时可以用三个问题快速判断:

| 当前层动作 | 是否新增稳定 identity | 是否新增 source frame | 心智模型 |
|------------|----------------------|-----------------------|----------|
| 只把下层分类收敛到上层分类 | 否,只做映射 | 否 | reason 收敛,例如 `conv_err()` |
| 表达新的业务或架构语义 | 是 | 是 | 建立新语义边界,例如 `source_err(...)` |
| 只补充当前操作路径或字段 | 否 | 否 | 追加上下文,例如 `doing(...)` / `with_context(...)` |

这张表的重点是避免两个极端:把每一层都包装成新的错误叙事,或者把跨语义域的底层技术失败直接暴露给上层。

### 边界输出

在系统边界(HTTP handler、RPC 端点、CLI 入口、日志写入点)输出错误:选择输出格式,应用暴露策略,输出。

### 一个完整传播示例

下面是一次"提交订单"失败经过三种模式的完整路径。

第一步,数据库错误首次进入结构化系统。repository 层选择 data access 语义下的稳定分类,保留数据库错误为 source,添加操作上下文。

```text
repository.insert_order(order) -> Result<(), StructuredError<RepositoryClass>> {
    db.insert(order)
        .on_error(source_error) {
            return StructuredError {
                identity: "repository.connection_failed",
                class: RepositoryConnectionFailed,
                detail: "insert order failed",
                context: {
                    operation: "insert_order",
                    order_id: order.id,
                    component: "order_repository"
                },
                source: source_error
            }
        }
}
```

第二步,service 层跨到业务语义域。不把数据库连接失败暴露给上层,而是表达业务失败:提交订单依赖不可用,同时保留下层 repository 错误为 cause。

```text
service.submit_order(order) -> Result<(), StructuredError<ServiceClass>> {
    repository.insert_order(order)
        .on_error(repo_error) {
            return StructuredError {
                identity: "order.submit_dependency_unavailable",
                class: SubmitDependencyUnavailable,
                detail: "submit order failed",
                context: {
                    operation: "submit_order",
                    order_id: order.id,
                    tenant: order.tenant
                },
                source: repo_error
            }
        }
}
```

第三步,HTTP handler 到达边界。不重新解释错误,交给集中策略生成输出。

```text
handler.post_orders(req) -> HttpResponse {
    result = service.submit_order(req.order)

    if result is error {
        err = result.error
        identity = err.identity

        log_record = policy.to_log_record(err)
        metrics.record(policy.metric_labels(identity))

        return HttpResponse {
            status: policy.http_status(identity),
            body: policy.public_body(identity),
            retry_after: policy.retry_after(identity)
        }
    }
}
```

契约通道最终给边界的是 `order.submit_dependency_unavailable`,用于决定状态码、用户消息、重试建议和指标标签;诊断通道保留了 service detail、repository detail、上下文和原始数据库错误。调用方不需要知道数据库细节,排障方仍可追溯根因。

### 错误治理的生命周期

运行时传播只是错误治理的一部分。稳定分类进入生产后,还要经过观测和演进:

```text
detect -> classify -> enrich -> propagate -> project -> observe -> review/evolve
```

- detect:在失败发生处捕获原始错误或业务失败。
- classify:选择当前语义域下的稳定错误标识和分类。
- enrich:追加 detail、context 和 source,不污染契约通道。
- propagate:跨层传播时保留诊断链,必要时建立新语义边界。
- output:在 HTTP/RPC/CLI/log/metric 等边界按策略生成输出视图。
- observe:通过日志、指标、trace、report 观察错误分布和治理效果。
- review/evolve:根据生产反馈合并、废弃或新增错误标识,调整策略和文档。

这一步很重要:错误分类不是一次性建模。某个错误标识如果长期承载多种治理动作,说明分类过粗;某批错误标识只有文案差异、治理动作相同,说明分类过细。L2 之后的错误治理,需要用生产观测反向校准分类契约。

---

## 治理等级

错误治理成熟度分四个等级:

**L0:无治理**

- 错误类型散乱:`std::io::Error` / `String` / `Box<dyn Error>` / 自定义 enum 混用
- 边界输出拼接字符串
- 排障依赖 grep 日志

**L1:统一载体**

- 自有跨层路径返回同一结构模型
- 具备基本原因链,但跨层时仍可能被丢弃
- 仍没有稳定分类契约,同一失败在不同模块归类不一致
- 这只是统一表达和传播,还不是治理

**L2:稳定分类**

- 分类契约稳定,有文档定义
- 边界输出使用统一策略
- 原因链跨层完整保留
- 测试断言错误标识,而非错误消息

**L3:治理驱动**

- 错误分类直接映射到治理动作(重试、降级、告警、SLA)
- 边界策略可配置,不同环境可不同
- 错误指标进入监控系统
- 新错误类型需 review 才能加入

大多数团队在 L0 和 L1 之间。L1 到 L2 是最容易被低估的一步:不是把返回类型换成统一载体就结束了,而是要让团队对"哪些失败共享同一错误标识"、"哪些分类代表可重试"、"哪些错误可对外暴露"形成共同语义。Java 的异常机制可以帮助项目达到 L1,但它不会自动提供稳定分类、统一边界策略或测试约束。

从 L1 到 L2 需要:

- 标准化分类契约,明确稳定错误标识、分类语义、category 和治理含义。
- 梳理存量错误,把散落的字符串、技术异常和临时 enum 迁移到稳定分类。
- 建立边界策略,统一 HTTP/RPC/CLI/log/metric 输出规则。
- 建立测试规范,断言错误标识和治理决策,而非断言错误消息。
- 建立评审习惯,新增错误时讨论语义归属,而非只讨论能否编译。

进入 L2 后,测试不应只检查"返回了错误"。更有价值的测试矩阵包括:

- identity 是否稳定,且不依赖错误消息文本。
- category、retryable、visibility、HTTP 状态码是否符合策略。
- source chain 是否保留了下层根因。
- context 是否包含定位问题所需的关键字段。
- exposure 是否正确脱敏,用户响应是否没有泄露内部细节。
- HTTP/RPC/CLI/log/metric 等输出视图对同一错误标识是否一致。

L1 到 L2 不是局部重构,而是团队协作模式的变化:错误分类从个人实现细节变成共享工程语言。

L3 意味着错误治理进入组织流程。新错误类型需要 review,因为每个新稳定错误标识都可能影响告警、重试、SLA、用户文案、协议兼容和运维看板。到了这个阶段,错误分类变更应像 API 变更一样被管理:有命名规范、兼容性规则、策略映射、测试覆盖,也有废弃和迁移路径。

---

## 不适用场景

1. **小型项目、原型、脚本。** 边界少、生命周期短、错误在局部处理时,没必要引入分层治理。
2. **性能极端敏感的场景。** 结构化错误路径有分配、原因链和上下文采集、序列化等成本;静态类型语言中泛型或模板还可能增加编译时间和代码体积。
3. **错误不需要跨层传播。** 若所有错误都在一层内处理完毕,收益接近于零。

---

## 阶段性小结

以上完成了通用方法论:错误治理为什么重要、核心矛盾是什么、Wukong 错误治理模型如何组织失败信息。接下来进入 Rust 落地。

---

## 基于 Rust 的错误治理方案

Rust 适合做结构化错误治理,但 Rust 不会自动完成治理。`Result<T, E>`、`enum`、`?` 和 trait 解决的是错误表达与传播的语法;稳定错误标识、语义边界、诊断链保留、边界输出和桥接契约,仍需工程层面设计。

`orion-error` 把 Wukong 错误治理模型落成 Rust 基础设施:

```text
Result<T, StructError<R>>

R                 -> 契约通道的 reason / identity / category
StructError<R>    -> 运行时载体,承载 detail / context / source chain
ExposurePolicy    -> 边界输出策略
report / interop  -> 诊断与外部生态桥接
```

`R` 是当前语义域的错误分类契约。不同 bounded context、架构层或业务域可拥有自己的 `Reason` 类型,跨域传播通过显式转换表达语义边界。

下图展示一个错误从底层失败进入结构化体系、跨语义域传播、最终在边界输出的过程。抓住三点:

- 内部传播使用统一载体 `StructError<R>`。
- 同一错误同时携带契约通道和诊断通道。
- 边界层只按策略生成输出视图,不重新解释错误。

```mermaid
flowchart TB
    raw["原始失败<br/>IO / DB / Network / Parser"]
    repo["Repository 层<br/>StructError&lt;RepositoryReason&gt;"]
    service["Service 层<br/>StructError&lt;OrderReason&gt;"]
    boundary["系统边界<br/>HTTP / RPC / CLI / Worker"]

    raw -->|"首次进入<br/>source_err(reason, detail)"| repo
    repo -->|"跨语义域<br/>source_err(new_reason, detail)"| service
    service -->|"边界输出<br/>exposure(policy)"| boundary

    subgraph governance["契约通道:稳定、有限、可测试"]
        identity["identity<br/>order.submit_dependency_unavailable"]
        category["category<br/>biz / sys / conf / logic"]
        policy["policy<br/>status / retry / visibility / hints"]
    end

    subgraph diagnostic["诊断通道:保真、可追溯"]
        detail["detail<br/>submit order failed"]
        context["context<br/>operation / order_id / tenant / component"]
        source["source chain<br/>service -> repository -> database"]
    end

    service -.携带.-> identity
    service -.携带.-> category
    service -.携带.-> detail
    service -.携带.-> context
    service -.携带.-> source

    identity --> policy
    category --> policy
    policy --> boundary

    boundary --> user["对外响应<br/>稳定错误码 + 可公开消息"]
    boundary --> log["日志 / Report<br/>诊断摘要 + 脱敏上下文"]
    boundary --> metric["指标 / 告警<br/>identity + category"]
```

图中的关键点是:错误在内部传播时不被压扁成字符串;边界层根据策略生成用户响应、日志/report 和指标/告警三个输出视图。

### 五项原则的实现映射

| 方法论原则 | Rust / orion-error 落地方式 | 作用 |
|------------|----------------------------|------|
| 用统一载体承载契约与诊断信息 | 内部跨层传播统一使用 `Result<T, StructError<R>>` | 调用方面对同一种错误形状,分类空间由 `R` 参数化 |
| 让契约通道保持稳定 | 领域 reason 定义稳定 identity、category 和分类语义 | 调用方、监控、协议边界依赖错误标识,不依赖错误文案 |
| 让诊断通道跨层保真 | 使用 detail、context、source chain 保留底层原因与当前层解释 | 上层可以收敛分类,排障仍能追溯根因 |
| 在边界集中输出 | 通过 exposure policy 统一决定 HTTP/RPC/CLI/log/metric 输出 | 避免每个 handler 各自拼响应、各自决定脱敏 |
| 显式桥接外部生态 | report、redacted render、std error interop、protocol JSON 等路径显式转换 | 每次信息降级、脱敏、暴露都有清楚契约 |

### 设计规则一:按语义域定义 Reason

每个语义域定义自己的 reason 类型,而不是把全系统错误塞进一个巨大的全局 enum。

```rust
#[derive(Debug, Clone, OrionError)]
enum RepositoryReason {
    #[orion_error(identity = "repository.connection_failed")]
    ConnectionFailed,

    #[orion_error(identity = "repository.write_failed")]
    WriteFailed,

    #[orion_error(transparent)]
    General(UnifiedReason),
}

#[derive(Debug, Clone, OrionError)]
enum OrderReason {
    #[orion_error(identity = "order.submit_dependency_unavailable")]
    SubmitDependencyUnavailable,

    #[orion_error(identity = "order.invalid_state")]
    InvalidState,

    #[orion_error(transparent)]
    General(UnifiedReason),
}
```

对应前文的稳定分类契约:`repository.connection_failed` 是 data access 语义,`order.submit_dependency_unavailable` 是业务语义。两者可由同一底层失败触发,但不应混成同一分类。

### 设计规则二:首次进入时建立结构化错误

普通 IO、数据库、网络、解析错误首次进入治理体系时,需同时完成:选择当前层分类、给出 detail、保留底层 source。

```rust
fn insert_order(order: &Order) -> Result<(), StructError<RepositoryReason>> {
    let ctx = OperationContext::doing("insert_order")
        .with_field("order_id", order.id.to_string())
        .with_meta("component.name", "order_repository");

    db_insert(order)
        .source_err(RepositoryReason::ConnectionFailed, "insert order failed")
        .map_err(|err| err.with_context(ctx))?;

    Ok(())
}
```

不把底层错误转成字符串。底层错误是 source,`"insert order failed"` 是 repository 层 detail,`order_id` 和 `component.name` 是 context。

### 设计规则三:跨语义域时建立新边界

同一语义域内的分类收敛只做 reason 转换,不新增错误叙事。跨越到新业务语义域时,建立新语义边界,把下层结构化错误作为 source 保留。

```rust
fn submit_order(order: &Order) -> Result<(), StructError<OrderReason>> {
    let ctx = OperationContext::doing("submit_order")
        .with_field("order_id", order.id.to_string())
        .with_field("tenant", order.tenant.to_string());

    insert_order(order)
        .source_err(
            OrderReason::SubmitDependencyUnavailable,
            "submit order failed",
        )
        .map_err(|err| err.with_context(ctx))?;

    Ok(())
}
```

service 层不把 repository 的连接失败暴露给 handler,而是表达业务失败:提交订单依赖不可用。repository 错误仍在 source chain 中。

### 设计规则四:边界只做输出,不重新解释错误

HTTP handler、RPC 端点、CLI 入口和 worker 边界不应重新拼装错误语义,应把结构化错误交给集中策略统一生成响应、日志、指标和调试报告。

```rust
fn handle_submit(req: Request) -> HttpResponse {
    match submit_order(&req.order) {
        Ok(()) => HttpResponse::ok(),
        Err(err) => {
            let snapshot = err.exposure(&DefaultExposurePolicy);
            log_error(err.report());
            HttpResponse::from(snapshot)
        }
    }
}
```

边界有多个输出视图:对用户 redacted exposure,对开发者和 SRE 保留 report,对监控输出稳定 identity 和 category。

### 设计规则五:测试错误标识,而不是错误文案

进入 L2 后,测试约束稳定错误标识和治理决策,不约束错误消息。错误消息可优化、翻译、脱敏;错误标识和分类语义才是长期契约。

```rust
let err = submit_order(&order).unwrap_err();

assert_eq!(
    err.identity_snapshot().code,
    "order.submit_dependency_unavailable"
);

let exposed = err.exposure(&DefaultExposurePolicy);
assert_eq!(exposed.decision.http_status, 503);
```

这类测试倒逼团队维护稳定分类契约:新增错误要有标识,修改标识要考虑兼容,边界输出策略要有明确预期。

可运行完整示例参见 `orion-error/examples/order_case.rs`:解析层、用户层、存储层和订单服务层各自定义 reason,底层错误首次进入结构化体系,跨层传播通过 reason 收敛保留诊断链,最终在边界统一输出。这个示例主要展示 `conv_err()` 收敛路径;如果上层需要建立新的业务语义边界,应按本文规则使用 `source_err(...)` 把下层结构化错误保留为 source。

---

## 工业级应用验证:WarpParse

`orion-error` 是 Wukong 错误治理模型在 Rust 下的基础设施实现。基础设施还需要真实工业级系统验证:高吞吐、长链路、多角色、多边界、强观测要求的系统,才真正检验错误治理是否可用。

WarpParse 是 Orion 体系中面向高吞吐日志解析与 ETL 的核心引擎。根据 `wp-examples/benchmark/report/report_linux.md` 的 Linux 单机 benchmark,WarpParse 0.12.0 在 Nginx、AWS ELB、Firewall、APT Threat、Mixed Log 五类日志及 File -> BlackHole、TCP -> BlackHole、TCP -> File 三种拓扑下,对比 Vector-VRL 0.49.0 取得纯解析 1.56x-20.30x、解析+转换 1.34x-17.90x 的 EPS 倍数区间。

Benchmark 证明的是工业强度:高吞吐、多格式、多拓扑、解析与转换并存。它本身不证明错误治理质量。错误治理的价值,需从失败路径能否被定位、分类、输出和自动化处理来判断。

因此,WarpParse 对这套方法论的验证不在于吞吐数字本身,而在于复杂失败路径是否能被稳定表达:

- 规则错误能否定位到文件、行列和字段。
- 配置错误能否阻断发布,而不是触发系统故障告警。
- 数据质量错误能否聚合统计,而不污染系统错误。
- 运行时错误能否区分可重试、不可重试和需要人工介入。
- 用户视图、运维视图和调试视图是否来自同一个稳定错误标识。

在这类系统中,若规则语法错误只返回一段字符串:

```text
unexpected token at line 12
```

规则开发者仍需打开规则文件、定位行列、猜测出错字段、判断是语法问题还是样本不匹配。系统也难以基于文本稳定区分"配置错误"、"数据质量问题"和"运行时系统错误"。

引入 Wukong 错误治理后,同一次失败被表达为结构化信息:

```text
identity : rule.syntax
category : config
detail   : unexpected token in extractor expression
context  : {
  rule_file      : "rules/nginx.wpl",
  line           : 12,
  column         : 18,
  field          : "request_time",
  expected_token : "identifier",
  actual_token   : ")"
}
policy   : block rule activation, show repair hint, do not page SRE
```

这是方法论在 WarpParse 中被验证的关键:规则开发者拿到错误位置和修复线索;运行系统拿到稳定错误标识和治理策略;运维侧可把配置错误、数据错误、系统错误分开统计和告警。吞吐越高,失败路径越需要结构化能力,否则处理能力越强,错误扩散和排障成本也被同步放大。

### WarpParse 的错误治理结构

WarpParse 的错误处理覆盖规则开发、规则验证、运行时解析、管线执行、边界输出和运维观测的完整链路。下图按三层阅读:失败来源、契约与诊断承载、输出视图。

```mermaid
flowchart TB
    sample["样本日志<br/>Nginx / ELB / Firewall / APT / Mixed"]
    rule["WPL 规则<br/>字段提取 / 类型转换 / 富化"]
    check["规则验证<br/>syntax / sample / schema"]
    engine["解析运行时<br/>高吞吐 parse / transform"]
    pipeline["ETL 管线<br/>input -> parse -> transform -> output"]
    boundary["系统边界<br/>CLI / API / worker / report"]

    sample --> check
    rule --> check
    check -->|"规则可用"| engine
    engine --> pipeline
    pipeline --> boundary

    subgraph failure["失败来源"]
        syntax["规则语法错误"]
        mismatch["样本不匹配"]
        typeerr["类型转换失败"]
        dirty["脏数据 / 异常字段"]
        runtime["运行时 I/O / backpressure / resource"]
    end

    syntax --> check
    mismatch --> check
    typeerr --> engine
    dirty --> engine
    runtime --> pipeline

    subgraph governance_wp["契约通道"]
        wp_identity["稳定错误标识<br/>rule.syntax / parse.mismatch / transform.type / runtime.io"]
        wp_category["category<br/>config / data / system"]
        wp_policy["策略<br/>是否中断 / 是否跳过 / 是否告警 / 是否可重试"]
    end

    subgraph diagnostic_wp["诊断通道"]
        wp_rule_ctx["规则上下文<br/>rule file / line / field / pattern"]
        wp_sample_ctx["样本上下文<br/>sample id / input slice / expected field"]
        wp_runtime_ctx["运行时上下文<br/>source / sink / batch / offset / component"]
        wp_source["source chain<br/>parser -> engine -> pipeline"]
    end

    check -.生成.-> wp_identity
    engine -.生成.-> wp_identity
    pipeline -.生成.-> wp_identity

    check -.保留.-> wp_rule_ctx
    check -.保留.-> wp_sample_ctx
    engine -.保留.-> wp_rule_ctx
    engine -.保留.-> wp_source
    pipeline -.保留.-> wp_runtime_ctx

    wp_identity --> wp_policy
    wp_category --> wp_policy
    wp_policy --> boundary

    boundary --> user_view["规则开发者视图<br/>错误位置 + 修复提示"]
    boundary --> ops_view["运维视图<br/>指标 + 告警 + 失败分类"]
    boundary --> debug_view["调试视图<br/>脱敏上下文 + source chain"]
```

这张图表达的核心原则:WarpParse 的高性能解析和错误治理必须同时存在。`orion-error` 提供错误治理基础设施,WarpParse 验证了这套方法论在工业级高吞吐 ETL 系统中的可用性。

---

## 面向 AI 的工程化复用

Wukong 错误治理模型不应只停留在文档里。更有效的做法是把方法论、设计原则、crate/lib 使用规范、示例代码、反模式和迁移规则整理成可复用的 engineering skills。Orion 体系中的 skills 沉淀在 `orion-skills` 仓库:https://github.com/galaxio-labs/orion-skills

AI 可基于这些 skills 产出项目级错误设计文档。例如 Warp Insight 的错误处理系统设计文档:https://github.com/wp-labs/warp-insight/blob/main/doc/design/foundation/error-handling-system.md 。这类文档的价值不只是记录错误类型,而是让 AI 和工程师围绕同一套治理模型讨论分类、传播、边界输出、观测和迁移。

这样 AI 不再只是临时生成几段错误处理代码,而是围绕一套明确的治理模型工作。面对新项目时,AI 先识别错误边界、语义域、稳定错误标识、诊断链和边界输出,再给出治理规划;进入实现阶段时,按约定使用 `orion-error`,把 reason 定义、source 保留、context 挂载、exposure 策略和测试断言落到代码里。

skills 把错误治理从"靠经验提示 AI"变成"给 AI 一套工程约束":

- 规划阶段:识别 L0/L1/L2 现状,设计分类契约和迁移路径。
- 设计阶段:划分语义域,定义 reason、identity、category 和治理属性。
- 实现阶段:选择首次进入、跨层收敛、语义边界包装和边界输出的正确 API。
- Review 阶段:检查是否丢失 source、是否依赖错误文案、是否在 handler 中重复拼响应、是否缺少错误标识测试。
- 迁移阶段:把字符串错误、临时 enum、泛化包装逐步收敛为稳定的契约与诊断结构。

错误处理横跨架构、协议、观测、测试和团队规范,单靠一次 prompt 难以稳定完成。把方法论和库约束沉淀为 skills 后,AI 才能在不同项目中复用同一套工程判断,生成一致、可维护的实现代码。

---

## 附录:语言机制与生态采纳

方法论与语言无关,但不同语言落地成本不同。区分两个维度:

- **语言表达能力**:语言是否方便表达稳定分类、结构化载体、原因链和边界输出。
- **生态采纳成本**:团队在既有生态中采用这套治理需要付出的组织和迁移成本。

亲和度高不等于采纳容易。Rust 类型系统非常适合这套模型,但错误生态路径较多;Go 类型表达能力弱,但显式 error 返回高度统一,引入轻量分类规范的组织成本反而可能更低。

无论使用哪种语言,落地时都应回答同一组问题:

- 稳定错误标识放在哪里,是否能跨版本保持兼容?
- 诊断链如何保留,跨层转换时是否会丢失根因?
- context 和 detail 放在哪里,是否会污染治理分类?
- 边界策略在哪里集中,HTTP/RPC/CLI/log/metric 输出是否一致?
- 进入日志、协议、标准错误接口或第三方框架时,哪些信息被保留、脱敏或丢弃?

### Rust — 原生匹配

Rust 同时满足三项:代数类型(`enum`)表达分类,`match` 提供穷尽检查;泛型提供类型安全的载体参数化;无异常机制,错误通过返回值传递,自然与载体配合。

但 Rust 的实际采纳并不简单。生态中长期存在 `failure`、`error-chain`、`anyhow`、`thiserror`、`eyre` 等不同取向:有的偏快速传播,有的偏诊断报告,有的偏领域错误定义。团队仍需明确边界:哪些层用结构化治理错误,哪些边界允许快速错误聚合,哪些错误标识进入长期契约。

### TypeScript — 亲和度高

```typescript
type AppErrorClass =
  | { kind: "not_found"; id: string }
  | { kind: "system_error" };
```

Union type + discriminated union 天然适合错误分类。`neverthrow`、`fp-ts` 的 `Either` 等库提供了返回值式错误处理。弱点是运行时类型信息有限,跨进程、跨包、跨 JSON 边界时仍需显式 runtime tag、schema 或协议字段保存错误标识和分类。

### Swift — 亲和度高

代数类型(enum with associated values)表达错误分类。`Result<T, E>` 在 Swift 5.0+ 中原生支持。社区中有用 `Result` 替代 `throws` 的实践。

### C# — 需要映射到异常生态

泛型支持良好(运行时保留类型信息),但异常机制主导生态。缺原生 discriminated union(可用 `OneOf` 模拟)。更自然的映射不是强行改成 Result,而是用异常类型层次表达分类、inner exception 保留原因链、ASP.NET Core 中间件做集中策略。

### Java — 需要映射到框架约定

泛型擦除,异常机制主导。但 cause chain 机制成熟,Spring 的 `@ControllerAdvice`、filter、interceptor 已是集中策略的常见模式。Java 17+ 的 sealed class、record 和 pattern matching 让有限分类表达比过去更自然。

**核心映射:每个语义域定义独立的 sealed class,域之间无继承关系。** 和 Rust 中 `RepositoryReason` 与 `OrderReason` 是两个独立 enum 同理,Java 中 `RepositoryError` 与 `OrderError` 是两个独立 sealed class。跨域时**构造新域的异常,把旧域异常作为 cause 保留**,而非向上转型到共同父类。

```java
// data access 语义域(精简示意)
public sealed abstract class RepositoryError extends RuntimeException
    permits RepositoryError.ConnectionFailed, ... {

    public abstract String identity();  // e.g. "repository.connection_failed"
    public abstract String category();  // e.g. "system"
    public abstract boolean retryable();

    private DiagnoseContext ctx;
    protected RepositoryError(String detail, Throwable cause, DiagnoseContext ctx) {
        super(detail, cause);
        this.ctx = ctx;
    }

    public static final class ConnectionFailed extends RepositoryError {
        public ConnectionFailed(String detail, Throwable cause, DiagnoseContext ctx) { super(detail, cause, ctx); }
        public String identity() { return "repository.connection_failed"; }
        public String category() { return "system"; }
        public boolean retryable() { return true; }
    }
}
```

跨域传播时,Service 层构造 `OrderError`,将 `RepositoryError` 作为 cause:

```java
catch (RepositoryError e) {
    throw new OrderError.DependencyUnavailable("submit order failed", e, ctx);
}
```

此时 cause chain:`OrderError.DependencyUnavailable → RepositoryError.ConnectionFailed → SQLException`。边界层由 `@ControllerAdvice` 集中路由:`@ExceptionHandler(OrderError.class)` 基于 `identity()` 决定状态码和响应体。

| 概念 | Rust | Java |
|------|------|------|
| 语义域分类 | `enum RepositoryReason` | `sealed class RepositoryError` |
| 契约通道 | reason 变体 + identity 字符串 | 子类覆写的 `identity()` / `category()` / `retryable()` |
| 诊断通道 | `StructError` 的 detail / context / source | `getMessage()` / context record / `getCause()` |
| 统一载体 | `StructError<R>` 泛型参数化 | **不可行** —— JLS 禁止泛型类继承 Throwable |
| 跨域转换 | `source_err(...)` 一行 | 显式 try-catch 构造新异常 |

Java 的硬约束:JLS 禁止泛型类继承 Throwable,因此无法用单一 `StructuredError<R>` 统一所有域;跨域转换必须显式 try-catch。核心架构思想一致:独立语义域、稳定错误标识为主键、诊断链通过 cause 保留、边界策略集中路由。

### C++ — 技术可行,生态无约定

模板保留类型信息,`std::expected`(C++23)提供类似 `Result` 的机制,Boost.Outcome 等库也提供了更完整的结果/错误建模。但 C++ 错误处理长期并存异常、错误码、expected、Outcome、自定义 status 等多条路径,生态无主导载体。技术可行,组织统一成本高。

### Go — 需要更多团队约束

`error` 接口默认只要求 `Error() string`,结构化信息需通过自定义 error 类型、`errors.Is`/`errors.As` 和 wrapping 额外建立。Go 不是不能做错误治理,而是生态默认路径偏轻量包装,治理约束需团队主动设计。

### 两个维度的对比

| 语言 | 语言表达能力 | 生态采纳成本 | 主要原因 |
|------|--------------|--------------|----------|
| Rust | 高 | 中 | 类型系统匹配,但错误生态路径较多,需团队约定边界 |
| Swift | 高 | 中 | enum/Result 表达自然,但 `throws` 仍是重要生态路径 |
| TypeScript | 中高 | 中 | discriminated union 方便,但运行时需 schema/tag 补足 |
| C# | 中 | 中 | 泛型和中间件成熟,但异常生态主导,DU 需模拟 |
| Java | 中 | 中 | cause chain 和框架边界成熟,sealed class 改善分类表达 |
| C++ | 中高 | 高 | 类型能力强,但错误处理路径分裂,组织统一成本高 |
| Go | 低中 | 中低 | 类型表达较弱,但显式 `error` 返回高度统一,轻量分类规范易推广 |

这个表只描述落地摩擦,不评价语言优劣。真正决定错误治理质量的,往往不是语言本身,而是团队是否建立了稳定错误标识、诊断保留、边界策略和演进规则。

---

## 总结

错误处理是系统从原型走向工业级应用的分水岭。原型只需证明正确路径能跑通;工业级系统须在输入变化、依赖退化、配置漂移、数据异常、规则演进和运行环境波动下继续保持可运行、可诊断、可恢复、可演进。

本文提出的 **Wukong 错误治理模型** 把错误信息拆成两条通道:

- **契约通道**:稳定错误标识、稳定分类、category、retryable、暴露等级,用于调用方决策、协议输出、监控告警、SLA 统计和长期兼容。
- **诊断通道**:原因链、上下文、细节、底层错误,用于排障、调试、规则修复、运行观测和系统演进。

两条通道解决的核心矛盾:错误在治理层面必须收敛,否则无法自动化决策;在诊断层面必须保真,否则无法定位根因。成熟的错误体系不能只追求"包装得漂亮",也不能只依赖语言机制,而要明确稳定错误标识如何演进、诊断链如何跨层保留、边界策略如何集中输出、外部生态桥接时保留和丢弃什么。

在 Rust 中,`orion-error` 将这套模型落成可复用基础设施:`StructError<R>` 承载契约与诊断信息,领域 reason 提供稳定分类契约,source chain 和 context 保留诊断路径,exposure/report/interop 完成不同边界的输出和桥接。`orion-error/examples/order_case.rs` 给出了小型可运行例子,WarpParse 提供了工业级验证:在高吞吐 ETL 系统中,错误治理直接影响规则开发体验、运行时可观测性、边界输出质量和长期运维成本。

错误治理不是异常语法的附属品,也不是日志格式的局部优化。它是工业级系统的信息架构之一,也是系统对抗腐化与脆弱的骨架。只有当失败路径也具备稳定分类、完整诊断、集中输出和可演进契约时,系统才真正从"能跑"走向"可长期运行"。