结果与错误
本章说明框架如何表达成功与失败:原语层的 Result、Error 与 Maybe,错误码文法与种类,错误为什么只携带消息键,以及宿主如何把错误翻译成 HTTP Problem Details 等协议形态。适用于写操作、中间件与 Provider 的开发者,也供对接 API 的客户端参考。
框架没有统一响应信封,成功直接返回响应对象,失败返回 RFC 9457 Problem Details。
完整 API 见 Primitives 包。
Result 与 Result<T>
可恢复的失败(找不到、冲突、校验不通过、外部设施不可用)用 Result 表达,不抛异常。框架的端口都按这个约定返回:仓储的 AddAsync、缓存的 GetAsync、消息的 PublishAsync、操作的 HandleAsync。
| 类型 | 用途 |
|---|---|
Result | 没有返回值的成功或携带 Error 的失败 |
Result<T> | 成功时携带值(不能为 null),失败时携带 Error |
Maybe<T> | 类型层面的「可能没有」,default 即无值;不表达失败 |
创建与转换:
using XiHan.Framework.Primitives;
namespace Shop.Orders;
public static class OrderErrors
{
public const string NotFound = "SHOP_NOTFOUND_001";
}
public static class OrderMessages
{
public const string NotFound = "shop.order.not_found";
}
public sealed record OrderView(Guid Id, string Name);
public sealed class OrderLookup
{
private readonly Dictionary<Guid, OrderView> _orders = [];
public Result<OrderView> Find(Guid id)
{
if (_orders.TryGetValue(id, out var order))
{
// 值与错误都可以隐式转换成 Result<T>
return order;
}
return Error.NotFound(OrderErrors.NotFound, OrderMessages.NotFound, MessageArguments.Of(("id", id)));
}
}读取结果:
| 成员 | 说明 |
|---|---|
IsSuccess / IsFailure | 是否成功 |
Error | 失败时的错误,成功时为 null |
TryGetValue(out value) | 成功时取出值 |
ValueOr(fallback) / ValueOrThrow() | 取值或回退;ValueOrThrow 在失败时抛出 FrameworkException |
Map / Bind / Match | 链式转换 |
ToResult() | Result<T> 去掉值 |
ThrowIfFailure() | 失败时抛出,只用于启动期、配置期这类不可恢复的场合 |
default(Result) 与 default(Result<T>) 视为未初始化的失败(PRIMITIVES_VALIDATION_002),用来发现忘记赋值的分支。
错误码
ErrorCode = Domain "_" Kind "_" Sequence
Domain = 术语表的错误码域(KERNEL、PERSISTENCE、SECURITY …)或应用自定义的大写标识
Kind = VALIDATION | NOTFOUND | CONFLICT | DENIED | UNAVAILABLE | TIMEOUT | CANCELLED | UNEXPECTED | 领域自定义
Sequence = 三位数字,域内唯一,发布后不复用示例:PERSISTENCE_CONFLICT_001、SECURITY_DENIED_003、SHOP_NOTFOUND_001。ErrorCode.IsValid、ErrorCode.KindOf、ErrorCode.DomainOf 解析与校验错误码。
错误码面向程序,文案面向人:客户端只应依赖错误码,不解析文案。框架已登记的错误码以 XiHan Spec 的 conventions/error-codes.md 为准,各包页列出本包会返回的错误码。
Error
Error 不可变,由工厂方法创建,工厂方法校验错误码的 KIND 段与方法名一致:
| 工厂方法 | KIND |
|---|---|
Error.Validation(code, messageKey, details) | VALIDATION |
Error.NotFound(code, messageKey, details) | NOTFOUND |
Error.Conflict(code, messageKey, details) | CONFLICT |
Error.Denied(code, messageKey, details) | DENIED |
Error.Unavailable(code, messageKey, details) | UNAVAILABLE |
Error.Unexpected(code, messageKey, inner, details) | UNEXPECTED |
new Error(code, messageKey, details, inner) | 其他种类(TIMEOUT、CANCELLED 与领域自定义) |
| 成员 | 说明 |
|---|---|
Code / Kind | 错误码与它的 KIND 段 |
MessageKey | 消息键,文案在语言文件里 |
Details | 参数与细节,名字到值的字典;既是模板参数,也随 Problem Details 输出标量项 |
Inner | 内部错误 |
Message | 按进程默认区域渲染的文案,供日志使用 |
WithCode(code) | 换码,保留消息键与参数 |
错误只携带消息键
Error 的第二个参数是消息键常量,不是文案。文案放在所属包的 Localization/<区域>.json 里,宿主在 Egress 阶段按调用方的区域翻译:同一个错误,Accept-Language: zh-CN 的调用方收到中文,未指定的收到默认的英文。参数以 MessageArguments.Of(("name", value)) 传入,模板里写 {name};按位置的参数用 MessageArguments.Positional(...),模板里写 {0}。消息键文法、语言文件格式与 10 种内置区域见国际化。
Error.Literal(code, text) 携带现成的文字而不是消息键,只用于两种场合:从线上还原对端返回的错误、把异常转换成错误。正式包里 Error.* 的第二个参数必须是常量,架构测试强制这一点。
异常
异常只表达编程错误与配置错误:参数为 null、清单不合法、配置值非法、启动时连不上必需的外部设施。它们在启动期暴露,不翻译成调用方语言:
| 类型 | 用途 |
|---|---|
ArgumentException 及其子类 | 参数与配置值非法 |
FrameworkException(code, message, inner) | 带错误码的框架异常;ToError() 转成 Error(文本以 Literal 承载) |
调用期间未处理的异常由 Observe 阶段捕获,记录日志并转成 KERNEL_UNEXPECTED_001 结果,调用方收到 500,不会看到异常细节。雪花标识生成器的 PERSISTENCE_UNAVAILABLE_004 / 005 例外:SnowflakeIdFeature 在 Resilience 阶段把它们转成同码的 UNAVAILABLE 结果,调用方收到 503,见标识生成。
翻译成协议
宿主读取管道结束后的结果,按错误码的 KIND 映射协议状态。HTTP 的映射(Hosting.AspNetCore 的 ProblemResponse.StatusCodeFor):
| KIND | HTTP 状态 |
|---|---|
VALIDATION | 400 |
NOTFOUND | 404 |
CONFLICT | 409 |
DENIED | 未认证(SECURITY_DENIED_001)与凭据无效(SECURITY_DENIED_003)为 401,并带 WWW-Authenticate 质询;其余为 403 |
UNAVAILABLE | 503 |
TIMEOUT | 504 |
CANCELLED | 408 |
UNEXPECTED 与其他 | 500 |
截止时间到了一律是 KERNEL_TIMEOUT_001(504),调用方同时取消、下游把取消写成 KERNEL_CANCELLED_001 结果也一样;408 只表示调用方在截止时间到达之前自己取消(见韧性)。
Problem Details 的字段:
{
"type": "urn:xihan:error:KERNEL_VALIDATION_003",
"title": "VALIDATION",
"status": 400,
"detail": "操作 'greetings.create' 的请求不符合契约:$.name: required property 'name' is missing",
"instance": "/greetings",
"code": "KERNEL_VALIDATION_003",
"traceId": "5978653a746fa20f0dce9f4a2f954bac",
"errors": { "name": ["缺少必填属性 'name'"] },
"details": { "id": "greetings.create", "reason": "$.name: required property 'name' is missing" }
}| 字段 | 内容 |
|---|---|
type | urn:xihan:error:<错误码> |
title | 错误码的 KIND |
status | HTTP 状态码 |
detail | 按调用方区域翻译的文案;响应头 Content-Language 给出命中的区域 |
instance | 请求路径 |
code | 错误码 |
traceId | 本次调用的 W3C trace-id |
errors | 只在校验错误时出现:字段名到文案列表 |
details | Error.Details 里的标量项 |
gRPC、MCP 与命令行的映射分别见 gRPC、MCP 与 CloudEvents 与命令行宿主:gRPC 以状态码加 xihan-error-code 尾部元数据表达,MCP 以 isError 结果表达,命令行以按 KIND 的退出码加 stderr 上的 Problem JSON 表达。
在端点里返回错误
最小 API 端点在 Handle 阶段内执行,把失败写进调用上下文,由宿主统一翻译:
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Hosting.AspNetCore.Endpoints;
using XiHan.Framework.Kernel.Pipeline;
using XiHan.Framework.Primitives;
using XiHan.Framework.Profiles.Minimal;
var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan.UseMinimalProfile());
var app = builder.Build();
app.UseXiHan();
app.MapGet("/reports/{id}", (string id, HttpContext http) =>
{
// 结果写进上下文,Egress 由宿主翻译为 Problem Details
http.GetXiHanContext().ValueOrThrow().SetOutcome(
Error.NotFound("SHOP_NOTFOUND_002", "shop.report.not_found", MessageArguments.Of(("id", id))));
return Results.Empty;
});
app.Run();操作契约不需要这样做:HandleAsync 返回的 Result 就是调用结果。
