Skip to Content

失败(Fail)

失败是类型化的控制流:错误值经 fail 抛出,沿调用链向上传递,每个调用点都被编译器检查消费义务。语言没有异常:失败不会在类型之外悄悄发生,也不需要 try-catch 兜底。

fail 子句与抛出

可能失败的函数用 fail 子句声明错误集,子句写在返回类型之前(uses、fail、where、返回类型的顺序):

error NotFound(key: string); func MustLoad(key: string) fail NotFound -> int { if key == "黄金" { return 1; } fail new NotFound(key); }

fail 表达式; 抛出错误值并立即离开函数。fail 集是静态契约:体内实际抛出的错误集超出声明集会过不了编译。

消费义务与五种形态

调用一个 fail 函数,调用点必须处于消费上下文,漏消费编译期直接报错,报文把改法全列出来:

error MS4142: Call to failable function 'Risky' (fail Bad) is not consumed: wrap it in 'try?' / 'try!' / 'capture' / a 'try { ... } recover { ... }' block, or declare 'fail Bad' on 'Sum' (bare 'try' propagates and requires that declaration).

消费形态共五种:try(传播)、try?(失败得 null)、try!(断言成功)、capture(收成值)、块形 try { } recover { }。本页用一个会失败的函数贯穿示例(负数失败、正数翻倍):

error Bad(code: int, msg: string); func Risky(a: int) fail Bad -> int { if a < 0 { fail new Bad(a, "负数"); } return a * 2; }

try:传播形

try expr 不接住失败,而是把失败继续向上传,所在函数必须声明覆盖这个错误集:

func Sum(a: int, b: int) fail Bad -> int { val x = try Risky(a); val y = try? Risky(b); return x + (y ?? 0); }

try Risky(a) 的失败面进入 Sum 的义务集,所以 Sum 声明了 fail Bad。传播是「声明式上抛」,中途不改写错误值。

try? 与 try!

try? 把失败压成 null(返回 T?),配合 ?? 给默认;try! 断言一定成功,失败就地 crash:

print(try! Risky(21)); print(try? Sum(2, 3), try? Sum(-1, 3));
42 10 null

Sum(-1, 3) 内部 try Risky(-1) 传播失败,整个 Sum 失败,外层 try? 收到 null。

块形 try recover

多条语句共享一个消费点:块内任意 fail 都流向 recover,块尾表达式是 Success 载荷:

val v = try { val p = Risky(-1); p + 100 } recover { Success(x) => x, Failure(e) => e.code, }; print(v);
-1

Risky(-1) 抛 Bad(-1, "负数"),recover 的 Failure(e) 臂接住,e.code 读载荷字段。recover 强制穷尽:Success 与 Failure 两臂缺一不可,漏臂编译期就报。

错误名臂:按位置解构载荷

Failure(e) 是整值兜底臂;写成错误名臂可以按位置解构载荷,绑定名只在臂体内可见:

val named = try { val q = Risky(-7); q + 100 } recover { Success(x) => x, Failure(Bad(code, m)) => code, }; print(named);
-7

Failure(Bad(code, m)) 只接 Bad 错误并按位绑定载荷(code 得 -7)。

错误名臂带守卫:错误名不匹配该臂不命中,含祖先链,基类臂接得住派生值。错误集里有多种错误时,为每种错误写同名臂即可精确分流,穷尽性按错误集清点,漏写成员臂就补成员臂或写 Failure(_) 兜底。

capture 与 recover 表达式

capture expr 把结果收成 result<T, E> 值,可以装变量、传参、稍后消费。recover r { 臂 } 是独立的消费表达式:

val r = recover capture Risky(-9) { Success(x) => x, Failure(e) => e.msg, }; print(r);
负数

先 capture 存下,再按需 recover,两步分离适合「先收集、后统一处理」的流程。

臂值并集:错误值直接用

recover 的臂全是错误值时,整个表达式的类型收敛为错误并集(显示形如 Meh | Bad),可以直接读载荷字段、用 is / as 分型:

error Meh(n: int); error Bad(code: int, msg: string); errors LoadError = Meh | Bad; func Load(k: int) fail LoadError -> int { if k == 0 { fail new Meh(k); } if k < 0 { fail new Bad(k, "负数"); } return k; } func main() -> int { val e = recover capture Load(-5) { Success(v) => new Meh(v), Failure(Bad(code, m)) => new Bad(code, m), Failure(Meh(n)) => new Meh(n), }; print(e is Bad, e.code); return 0; }
True -5
  • e 装的是并集 Meh | Bad:e.code 直接读(字段至少存在于一个成员即可),e is Bad 分型。
  • Failure(err) => err 整值臂同样并入并集。

块形 try { ... } recover { ... } 的臂同样支持错误值出口:块尾表达式算出错误值时,它作为 Success 载荷交付,臂直通即可,不要重新包一层:

val e = try { val v = Load(7); new Meh(v) } recover { Success(m) => m, Failure(err) => err, }; print(e is Meh, e.n);
True 7

臂里 m 已经是 Meh,写 Success(m) => new Meh(m.n) 等于把错误值当其 int 字段传入,类型失配。混合臂(有的臂出正常值、有的臂出错误值)不收敛成并集:既要正常值兜底、又要错误值带出,用 capture + recover 表达式。

main 带错退出

main 也可以声明 fail 子句。失败未被消费时传播到顶层,程序以退出码 1 结束:

error Boom(); func main() fail Boom -> int { fail new Boom(); }
mush: 脚本失败: Main.Boom

错误名带模块限定,输出里的 Main.Boom 就是模块加错误名。

注意

  • try! 失败即就地 crash,表示断言该操作必定成功,与崩溃的语义衔接;
  • 已消费的错误值就是普通数据:可打印、可比较引用、可读 @site,深消费(错误名臂、继承臂)之外先掌握五种形态;
  • 裸 print 看不到失败通道,失败值必须经消费形态转换后才能作为普通值使用;
  • 结构体构造器同样可声明 fail 子句,此时 new 表达式是可失败调用,调用点须消费。
最后更新于 2026年10月9日