普贤教程 › 基础教程
错误处理
PuXian 没有 try / catch / throw。错误分成两类:可恢复的用返回值表达(Ok / Err),不可恢复的直接终止程序。这条分界线一旦习惯,代码会比异常版本更容易读。
为什么没有异常
- 函数签名不用标注
throws,但调用点必须显式处理Err——错误不会被悄悄跳过。 - 没有"从任意深度跳到某处"的控制流,读代码时不需要考虑隐藏的跳转。
- 代价是:忘记检查
is_err()就会继续往下跑,读到null或触发运行时错误。
Ok 与 Err
px
def parse_age(s): var t = trim(s) if len(t) == 0: return Err("年龄不能为空") var i = 0 while i < len(t): var c = t[i:i + 1] if c < "0" or c > "9": return Err("年龄必须是数字:" + s) i += 1 var n = int(t) if n > 150: return Err("年龄不合理:" + s) return Ok(n) var a = parse_age(" 42 ") print(str(a.is_ok())) print(str(a.ok())) var b = parse_age("4x2") print(str(b.is_err())) print(b.err())
text
true 42 true 年龄必须是数字:4x2
| 方法 | 返回 | 用途 |
|---|---|---|
is_ok() | bool | 成功判定 |
is_err() | bool | 失败判定 |
ok() | 值 | 取成功值(失败时是 null,别这样用) |
err() | 错误信息 | 取错误描述 |
unwrap() | 值 | 取成功值;对 Err 调用会终止程序 |
px
var r = Err("坏了") print(str(r.unwrap()))
text
运行时错误: 错误 [R1004] 2:19: unwrap 失败: Err(坏了)
unwrap 不是"懒得处理"的出口它对 Err 会直接终止进程。只有在"这里绝不可能失败,失败就是 bug"的地方才用(比如刚校验过的内部数据)。
调用链上的传播
每一层都可以把 Err 原样往上抛,保留上下文:
px
def read_port(conf): if not conf.has("port"): return Err("配置缺少 port") var p = conf["port"] if p < 1 or p > 65535: return Err("端口超出范围:" + str(p)) return Ok(p) def start(conf): var r = read_port(conf) if r.is_err(): return Err("启动失败:" + r.err()) return Ok("监听 " + str(r.ok())) print(start({"port": 8080}).ok()) print(start({}).err())
text
监听 8080
启动失败:配置缺少 port不可恢复的运行时错误
这些错误无法被捕获,只能避免:
| 错误码 | 含义 | 典型触发 |
|---|---|---|
| R1001 | 未定义变量 | 拼错名字、import 没生效 |
| R1002 | 类型不匹配 | "x" + 1、int("abc") |
| R1004 | unwrap 失败 | 对 Err 调 unwrap() |
| R1007 | 类型没有该字段/方法 | xs.insert(...)、str.status |
| R1008 | 字典没有该键 | d["missing"] |
| E2001 | 语法错误 | 漏冒号、缩进错、用了保留字当变量名 |
| i_call_builtin | 内建函数内部失败 | 如 json_parse 收到坏 JSON、写文件失败 |
陷阱:会抛出的内建函数
json_parse 遇到坏输入是不可捕获的运行时错误,本意想返回 400 的代码会变成 500。永远用 json_parse_opt:
px
var bad = json_parse_opt("{oops") print(str(bad.is_err())) print(bad.err())
text
true
json: 期望对象键同类还有 int() / float() 的严格解析、hex_to_bytes 的畸形输入。处理外部数据时,先校验再调用。
错误信息的写法
| ❌ 不好 | ✅ 好 |
|---|---|
Err("失败") | Err("端口超出范围:70000") |
Err(str(e)) | Err("读取配置失败:" + e.err()) |
| 把内部堆栈写进错误 | 写"哪个字段、什么值、期望什么" |
错误信息最终会出现在日志或接口响应里。写给人看,不要写给机器看。
一个完整例子
px
def load_conf(path): if not exists(path): return Err("配置文件不存在:" + path) var r = read_file_opt(path) if r.is_err(): return Err("读取失败:" + r.err()) var j = json_parse_opt(r.ok()) if j.is_err(): return Err("不是合法 JSON:" + j.err()) var conf = j.ok() if not conf.has("name"): return Err("缺少必填字段 name") return Ok(conf) print(load_conf("/tmp/nope.json").err())
text
配置文件不存在:/tmp/nope.json
约定团队里统一:返回
Ok(值) / Err(字符串),错误字符串不超过一行、以「动词+对象+原因」组织。这样日志 grep 起来很省事。