# 错误处理
Deft 的错误就是值 —— 具体来说是带标签的 `%Err{ :message ... }` 映射。本页介绍
`try`/`catch`、`throw`、Result 类型(`%Ok`/`%Err`),以及用于串联返回 Result 的
代码的组合子与传播形式。
> 设计说明: Ok/Err 是普通的带标签映射。它们原封不动地流经 `set`、
> 函数参数和管道 —— 不会自动解包,也不会隐式短路。如何处理 Result
> 始终由调用方决定。显式工具(`err!`、`and-then`、`unwrap`、`match`)
> 负责解包。
## 三种工具
| 工具 | 使用时机 |
|---|---|
| `try` / `catch` | 你可以在本地从失败中恢复。 |
| Result(`%Ok` / `%Err`) | 失败是一个值 —— 由调用方决定如何处理。 |
| `throw` | 调用方不应被期望处理的真正异常状况。 |
## `try` / `catch`
`try { body } catch name { handler }` 捕获任何抛出的错误,并将其绑定到
`name`:
def safe-read {|path^string| try { set raw [unwrap [fs/read $path]] [json/parse $raw] } catch e { echo "failed: $e~message" %{} } }
捕获到的值是一个 `%Err{ :message ... }` 映射。用 `~` 访问其字段。
### 块中的多条语句
`try` 和 `catch` 块都可以包含任意数量的语句;最后一条语句的值就是该块的值:
try { set raw [unwrap [fs/read $path]] set parsed [json/parse $raw] parsed~users } catch e { log/error "parse failed" $e @{} }
### 重新抛出
try { [dangerous-op] } catch e { if ($e~message == "transient") { [retry] # swallow and retry } else { [throw $e] # re-throw } }
## `throw`
`throw` 抛出一个 `%Err`。在 `try` 内部,它会被捕获;在外部,它会中止脚本
(在 TUI 宿主中则是中止面板的应用线程)。
def divide {|a b| if ($b == 0) { [throw "division by zero"] } ($a / $b) }
你可以抛出一个字符串(成为 `%Err` 的 `:message`),也可以抛出带额外字段的
完整 `%Err` 映射:
[throw %Err{ :message "invalid input" :code 422 :field "email" }]
## Result 类型
对于可预测、可恢复的失败,优先使用 Result 类型而不是异常。Result 要么是
`%Ok{ :value ... }`,要么是 `%Err{ :message ... }`。它们是普通的带标签
映射 —— 原封不动地流经 `set`、函数参数和管道。
### 构造 Result
%Ok{ value 42 } # literal tagged-map construction
%Err{ message "nope" }
[ok 42] # constructor functions (equivalent)
[err "nope"]
当需要包装一个计算出的值时,`[ok]` 和 `[err]` 很方便:
def safe-parse {|raw^string| if [empty? $raw] { %Err{ message "empty input" } } else { try { %Ok{ value [json/parse $raw] } } catch e { $e # already an %Err } } }
### 谓词
[ok? $r] # true if %Ok [err? $r] # true if %Err
### 解包
`unwrap` 提取 `%Ok` 的值,或为 `%Err` 返回一个默认值:
set v [unwrap $r 0] # the value if Ok, 0 if Err set v [unwrap $r] # the value if Ok, the message if Err
要进行显式的分支处理,请使用 `match`:
match $r { %Ok{ value $v } => [process $v] %Err{ message $m} => [log/error $m] }
## 标记返回 Result 的函数
### `def name^Result`(受检查)
显式的返回类型标注。编译器会把该约定记录在闭包上;在运行时,每条返回路径
都会被检查 —— 如果返回的值不是 `%Ok` 或 `%Err`,就会向 stderr 记录一条
警告。
def load-config^Result {|path^string| if [nil? $path] { return [err "no path"] } %Ok{ value $path } } def buggy^Result {|x| if ($x == 0) { return "zero" } # ⚠ warns: ^Result fn returned string %Ok{ value $x } }
该检查是运行时(而非静态)的 —— 当执行真正到达有问题的返回处时才会触发。
对未标注的函数开销很小(每个返回处一个分支,用于跳过检查)。目前 `^Result`
是唯一被强制执行的返回类型;其他标注(`^string`、`^number`)仅作文档用途。
## 串联返回 Result 的操作
两种互补的风格:`ok!` 用于命令式序列,`result/*` 组合子用于函数式管道。
### `ok!` 和 `err!` 断言函数
这一对标准库函数会断言 Result 的变体,并提取其载荷或从外层函数提前返回:
| 形式 | `%Ok` | `%Err` | 非 Result |
|---|---|---|---|
| `[ok! expr]` | 解包为 `:value` | 提前返回 Err | 原样传递 |
| `[err! expr]` | 提前返回 Ok | 提取 `:message` | 原样传递 |
`ok!` 是推进正常流程的代码的常用形式 —— “我期望这一步成功;把值给我,
否则就退出。”`err!` 是它在错误处理路径上的对称对应 —— “我期望这一步已经
失败;把消息给我。”
两者各有两种等价的语法形式:
[ok! [fs/read $path]] # bracket form [fs/read $path](ok!) # postfix form — same semantics [err! $r] # bracket form [$r](err!) # postfix form
后缀形式读起来很自然,即“对结果调用 ok!” —— 在调用处就可见,不会因为
行尾的小符号而看漏。
def load-config^Result {|path^string| set raw [ok! [fs/read $path]] # unwrap Ok, or propagate Err set parsed [ok! [json/parse $raw]] %Ok{ value $parsed } } def report-failure^Result {|r| set msg [err! $r] # extract Err message, or propagate Ok [log "got error: $msg"] }
两者都可以直接作用于变量 —— 无需包装:
def process^Result {|r| set v [ok! $r] # unwraps Ok, propagates Err ($v + 1) }
`ok!` 和 `err!` 是编译器内建函数(intrinsic),不是普通函数 —— 它们不能被
重定义。提前返回语义需要编译器支持(运行时函数无法退出其调用方)。如果只
想要纯粹的解包而不提前退出,请改用 `[unwrap $r]` 或 `[unwrap $r default]`。
### 标准库组合子(函数式风格)
| 形式 | 用途 |
|---|---|
| `[result/map $r $f]` | 将纯函数提升到 Result 中:`Ok(v)` → `Ok(f(v))`,Err 会传播 |
| `[and-then $r $f]` | 单子绑定(monadic bind,即 and-then):`Ok(v)` → `f(v)`,Err 会传播。`f` 必须返回 Result |
| `[result/traverse $coll $f]` | 在集合上映射 `f`,遇到第一个 Err 时短路。返回 `Ok([values])` |
| `[result/sequence $coll-of-results]` | 将 `[Result a]` 变成 `Result [a]`,遇到第一个 Err 时短路 |
# bind: chain Result-returning steps set r [and-then [op-a $x] {|a| [and-then [op-b $a] {|b| [ok ($a + $b)]}]}] # traverse: validate a list, bail on first failure set r [result/traverse $inputs {|x| [validate $x]}] match $r { %Ok{ value $validated } => [process-all $validated] %Err{ message $m } => [log/error $m] }
对于命令式序列,`ok!` 通常更清晰;对于函数式管道,`and-then` 和
`result/traverse` 通常更清晰。
## 我该用哪一种?
### 本地恢复 → `try` / `catch`
def parse-or-default {|raw| try { [json/parse $raw] } catch _e { %{} } }
### 把决定权上交 → Result
def fetch-user^Result {|id^string| # caller decides how to handle %Ok vs %Err [http/get "$api/users/$id"] }
### 首次失败即退出 → `ok!`
def load-config^Result {|path^string| set raw [ok! [fs/read $path]] set parsed [ok! [json/parse $raw]] %Ok{ value $parsed } }
### 校验集合 → `result/traverse`
def validate-all^Result {|inputs| [result/traverse $inputs {|x| [validate $x]}] }
### 真正的异常 → `throw`
def assert-positive {|n^number| if ($n < 0) { [throw "expected positive, got $n"] } $n }
## 约定
- 大多数可能失败的标准库调用都会返回 Result。用 `err?`、`unwrap`、
`match` 或 `err!` 检查。
- 违反不变式(程序错误)时抛出异常;预期内的失败(用户输入、网络等)返回
`%Err`。
- 用 `^Result` 标注返回 Result 的函数,以便运行时检查器能发现遗漏的包装。
- 如果要处理多种错误类型,请在针对 `%Err` 的 `match` 中包含 `_` 分支 ——
无匹配时 `match` 会抛出异常。
- `try` 块与 `defer` 能很好地组合 —— 即使抛出异常,延迟清理也会运行。
## 快速参考
| 形式 | 用途 |
|---|---|
| 构造 | |
| `[ok value]` / `%Ok{ value ... }` | 构造一个 Ok 结果 |
| `[err message]` / `%Err{ message ... }` | 构造一个 Err 结果 |
| 检查 | |
| `[ok? x]`, `[err? x]` | Result 谓词 |
| `[unwrap result ?default]` | 获取内部值(不提前退出) |
| `match $r { %Ok{ value $v } => ... %Err{ message $m } => ... }` | 解构 Result |
| 标记 | |
| `def name! {|args| ...}` | 非正式的 Result/可变标记(仅文档用途) |
| `def name^Result {|args| ...}` | 受检查的返回 Result 约定(违反时警告) |
| 传播 | |
| `[ok! expr]` 或 `[expr](ok!)` | 断言 Ok:解包 `:value` 或提前返回 Err |
| `[err! expr]` 或 `[expr](err!)` | 断言 Err:提取 `:message` 或提前返回 Ok |
| `[result/map $r $f]` | 将纯函数 `f` 提升到 Result |
| `[and-then $r $f]` | 单子绑定(and-then) |
| `[result/traverse $coll $f]` | 在集合上映射并短路 |
| `[result/sequence $coll]` | `[Result]` → `Result [...]` |
| 异常 | |
| `[throw value]` | 抛出一个错误 |
| `try { ... } catch e { ... }` | 捕获任何抛出的错误 |