跳到主要内容

错误处理

编译时错误

nature 是强类型语言,语法错误和类型异常都能在编译阶段发现,并给出可读的编译时错误提示,比如一个典型的类型不匹配错误

main.n
fn test():int {
var a = 12
return a as f32
}

编译

> nature build main.n
xxx/main.n:3:12: type inconsistency, expect=int(i64), actual=f32

可以看到错误的原因是第三行的 return 返回的类型是 f32, 但是函数期望得到的返回类型是 int。其中 3 表示行号, 12 表示第几列

运行时错误

运行时错误是一些无法在编译时进行识别的错误,最典型的例子就是对动态数组的越界访问

main.n
fn foo():int {
var list = [1, 2, 3] // 声明一个 vec 动态数组,其长度为 3
return list[4] // 进行越界访问
}

foo()

编译并运行

> ./main
catch error: 'index out of vec [4] with length 3' at nature-test/main.n:3:17
stack backtrace:
0: main.foo_0
at nature-test/main.n:3:17
1: main
at nature-test/main.n:6:0

第一行显示了运行时错误的原因以及具体位置,后续则显示了具体的错误调用栈方便进行错误排查。除了内置的运行时错误外,我们也可以通过 throw 关键字主动抛出运行时错误,下面将详细讲解运行时错误处理。

运行时错误处理

运行时错误处理是一个复杂的概念,需要考虑处理和无法处理的错误、预期和非预期的错误等等。

但是我们应该时时刻刻的在每一次 call 时关心错误么?其实并不需要,我们应该只关心我们能够处理的错误,对于不能处理或者预料之外的错误,我们没有必要去拦截或者处理它,应该将它继续向上传递,直到遇到一个能够处理这种错误的 caller。

fn call():int {
// logic...call->call1->call2->call3...
return 1
}

// call 的调用链可能非常的深,并存在了一个异常,比如有一个虫子钻进了内存中导致的内存访问异常
// 但是我只是一个小小的 caller,我能做的就是读取 call 中的数据,我无法处理类似虫子钻进了内存中导致的错误,所以只有当 call 能够返回时我才继续向下执行,否则我将不做任何的处理。
// 错误将沿着调用链向上级传递,直到遇到了一个能够处理这个错误的 caller
var foo = call()

在 nature 语言中,我们采用 throw 和 try 关键字以及 tup 类型来处理错误。使用 throw 关键字可以抛出错误,使得函数立即退出,并将错误信息传递到调用链上游。

fn rem(int dividend, int divisor):int {
if (divisor == 0) {
throw 'divisor cannot zero'
}

return dividend % divisor
}

// v 由于第二个参数为 0,会导致除数为 0,从而抛出异常。因为我们没有捕获该异常,它会继续向上传递,直到遇到 catch 块或程序退出。
// 因此,后面的语句 println('hello world') 不会被执行。
var result = rem(10, 0)
println('hello world')

通过输出我们发现,error 一直像上传递直到 runtime,runtime 拦截了这个错误并 dump 出来。println('hello world') 也和预期一样没有执行。

> ./main
catch error: 'divisor cannot zero' at nature-test/main.n:3:27
stack backtrace:
0: main.rem_0
at nature-test/main.n:3:27
1: main
at nature-test/main.n:11:22

我们再来看看使用 try 关键字主动拦截错误的情况

fn rem(int dividend, int divisor):int {
if (divisor == 0) {
throw 'divisor cannot zero'
}

return dividend % divisor
}

// v 对可能出现的错误使用 try 关键字进行拦截,nature 中默认不包含 null 值
// 当不存在错误时 err 是空的 errort 结构体,err.has 包含默认值 false
var (result, err) = try rem(10, 0)
if err.has {
// error handle, errort 结构中包含 msg 字段存储了错误的信息
println(err.msg)
} else {
println(result)
}

// v 不存在异常的情况下使用 try 拦截
(result, err) = try rem(10, 3)
if err.has {
println(err.msg)
} else {
println(result)
}

输出看看

> ./main
divisor cannot zero
1

try 关键字只能用于函数调用的前面,其读取本次函数调用是否 throw 了 error。

当原函数包含返回值时,try 将创建一个拥有两个元素的 tuple,第一个元素是函数原来的返回值,第二个元素则是 errort 类型的错误数据。 当原函数没有返回值时,catch 直接返回一个 errort 类型的数据。

信息

当函数没有返回值时,可以这么理解,由于 nature 不支持单元素的 tuple,所以 var (err) = try void_fn() 降级为 var err = try void_fn()

这是 errort 类型的定义

type tracet = struct {
string path
string ident
int line
int column
}

type errort = struct {
string msg
[tracet] traces
bool has
}

不仅是在函数调用中,try 后面可以接更多更长的表达式。本质上和传统语言的 try catch 没有什么区别,只是稍微简化了一下语法糖。

var (foo, err) = try foo[1] // v index out of range
var err = try foo().bar().car() // v 链式调用
var (bar, err) = try foo as int // v union assert 断言异常时
var (car, err) = try foo.bar[1] // v 链式调用

💡 观察上面的代码可以发现,err 是可以重复定义的, 规则是只要是 try 表达式的对应的错误信息接受变量就不会进行重复定义的检测。

🎉 相信你已经掌握了 throw 和 try 语法关键字的使用,这就是 nature 中错误处理的所有语法概念。语法简单不代表错误处理是一件简单的事情,它涉及到如何在程序中设计、捕获、记录和处理错误,是编写健壮、可靠和高质量软件的关键。