px普贤教程PuXian
普贤教程 › 基础教程

错误处理

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")
R1004unwrap 失败对 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 起来很省事。