Erlang 最初被设计用来编写容错式系统,这种系统原则上应该永不停歇。这就意味着运行时的错误处理是至关重要的。
处理顺序代码里的错误
每当我们在 Erlang 里调用某个函数后,返回或者返回一个值,或者出现了问题。
%% shop.erl cost(oranges) -> 5; cost(newspaper) -> 8; cost(apples) -> 2; cost(pears) -> 9; cost(milk) -> 7.
运行函数发生的现象:
1> c(shop).
{ok,shop}
2> shop:
cost/1 module_info/0 module_info/1
2> shop:cost(apples).
2
3> shop:cost(socks).
** exception error: no function clause matching shop:cost(socks) (shop.erl, line 4)
当我们调用了 cost(socks) 后程序崩溃了,发生这种情况的原因是函数定义里没有任何一个子句能匹配调用的参数。
异常错误发生于系统遇到内部错误时,或者通过在代码里显式调用 throw(Exception)、exit(Exception) 或 error(Exception) 触发。会触发异常错误的调性内部错误有模式匹配错误,用错误类型的参数调用内置函数,以及用带有错误值的参数调用内置函数。
可以通过调用下面的某个内置函数来显式生成一个错误。
exit(Why) 当你确实像要终止当前进程时就用它。
throw(Why) 抛出一个调用者可能想要捕捉的异常错误。
error(Why) 它与系统内部生成的错误差不多。
Erlang 有两种方法来捕捉异常错误。第一种是把抛出异常错误的调用函数封装在一个 try...catch 表达式里,另一种是把调用封装在一个 catch 表达式里。
用 try...catch 捕捉异常错误
语法如下:
try FuncOrExpressionSeq of Pattern1 [when Guard1] -> Expressions1; Pattern2 [when Guard2] -> Expressions2; ... catch ExceptionType1: ExPattern1 [when ExGuard1] -> ExExpressions1; ExceptionType2: ExPattern2 [when ExGuard2] -> ExExpressions2; ... after AfterExpressions end
try...catch 具有一个值
Erlang 里一切都时表达式,而表达式都具有值。因此我们可以编写这样的代码:
f(...) -> ... X = try ... end, Y = g(X), ...
更多情况下不需要 try...catch 表达式的值。所以只需要这么写:
f(...) -> ... try ... end, ... ...
try...catch的工作方式如下: 首先执行 FuncOrExpessionSeq。如果执行过程没有抛出异常错误,那么函数的返回值就会与 Pattern1、Pattern2 等模式进行匹配,直到匹配成功。如果能匹配,那么整个 try...catch 的值就通过执行匹配模式之后的表达式序列得出。
如果 FuncOrExpressionSeq 在执行中抛出了异常错误,那么 Expattern1 等捕获模式就会与它进行匹配。ExceptionType 是一个原子(throw、exit 和 error),它告诉我们异常错误是如何产生的。如果省略了 ExceptionType 就会使用默认值 throw。
关键词 after 之后的代码是用在 FuncOrExpreesionSeq 结束后执行清理的。这段代码一定会被执行,那么有异常抛出也是如此。after 区块的代码会在 try 或 catch 区块里的 Expression 代码执行完成后立即执行。AfterExpressions 的返回值会被丢弃。
简写法
可以省略 try...catch 表达式的多个部分。
try F catch ... end
就等于这一段:
try F of Val -> Val catch ... end
除此之外,after 部分也可以省略。
try...catch编程样例
设计应用程序时,如果某段代码的作用是捕捉错误,那么通常会设法确保它能捕捉到函数可能产生的一切错误。
下边这两个函数对此进行了演示。第一个函数会产生三种不同类型的异常错误,并有两个常规的返回值。
%% try_test.erl generate_exception(1) -> a; generate_exception(2) -> throw(a); generate_exception(3) -> exit(a); generate_exception(4) -> {‘EXIT‘, a}; generate_exception(5) -> error(a).
现在封装一个函数,用它在一个 try...catch 表达式里调用 generate_exception。
demo1() -> [catcher(I) || I <- [1,2,3,4,5]]. catcher(N) -> try generate_exception(N) of Val -> {N, nomal, Val} catch throw:X -> {N, caught, thrown, X}; exit:X -> {N, caught, exited, X}; error:X -> {N, caught, error, X} end. generate_exception(1) -> a; generate_exception(2) -> throw(a); generate_exception(3) -> exit(a); generate_exception(4) -> {‘EXIT‘, a}; generate_exception(5) -> error(a).
它展现了捕捉与区分一个函数所能抛出的所有异常错误形式。
1> c(try_test).
{ok,try_test}
2> try_test:demo1().
[{1,nomal,a},
{2,caught,thrown,a},
{3,caught,exited,a},
{4,nomal,{‘EXIT‘,a}},
{5,caught,error,a}]
用 catch 捕捉异常错误
另一种捕捉异常错误的方法是使用基本语法 catch。catch 语法和 try...catch 里的 catch 区块不是一回事。异常错误如果发生在 catch 语句里,就会被转换成一个描述此错误的 {‘EXIT‘, ...} 元组。
%% try_test.erl demo1() -> [catcher(I) || I <- [1,2,3,4,5]]. demo2() -> [{I, (catch generate_exception(I))} || I <- [1,2,3,4,5]]. catcher(N) -> try generate_exception(N) of Val -> {N, nomal, Val} catch throw:X -> {N, caught, thrown, X}; exit:X -> {N, caught, exited, X}; error:X -> {N, caught, error, X} end. generate_exception(1) -> a; generate_exception(2) -> throw(a); generate_exception(3) -> exit(a); generate_exception(4) -> {‘EXIT‘, a}; generate_exception(5) -> error(a).
两种方法提供了不同量级的调试信息。
1> try_test:demo2().
[{1,a},
{2,a},
{3,{‘EXIT‘,a}},
{4,{‘EXIT‘,a}},
{5,
{‘EXIT‘,{a,[{try_test,generate_exception,1,
[{file,"try_test.erl"},{line,23}]},
{try_test,‘-demo2/0-lc$^0/1-0-‘,1,
[{file,"try_test.erl"},{line,8}]},
{try_test,‘-demo2/0-lc$^0/1-0-‘,1,
[{file,"try_test.erl"},{line,8}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,673}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,641}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,626}]}]}}}]
针对异常错误的编程样式
改进错误消息
内置函数 error/1 的一种用途是改进错误消息的质量,如果在调用 math:sqrt(X) 时提供了一个负值的参数,就会发生下面的情况:
1> math:sqrt(-1).
** exception error: an error occurred when evaluating an arithmetic expression
in function math:sqrt/1
called as math:sqrt(-1)
编写一个封装函数来改进错误消息。
%% lib_misc.erl sqrt(X) when X < 0 -> error({squareRootNegativeArgument, X}); sqrt(X) -> math:sqrt(x).
1> lib_misc:sqrt(-1).
** exception error: {squareRootNegativeArgument,-1}
in function lib_misc:sqrt/1 (lib_misc.erl, line 5)
经常返回错误时的代码
如果你的函数并没有什么通用的情形,那么多半应该返回 {ok, Value} 或 {error, Reason} 这类值,但是请记住,这将迫使所有的调用者必须对返回值做点什么。
第一种:
... case f(X) of {ok, Val} -> do_some_thing_with(val); {error, Why} -> %% ... 处理这个问题 ... end, ...
第二种:
... {ok, Val} = f(X), do_some_thing_with(val); ...
如果 f(X) 返回 {error, ...} 就会抛出一个异常错误。
错误可能有但罕见时的代码
这种情况下,通常要写能处理错误的代码,就像这个例子一样:
try my_fun(X) catch throw:{thisError, X} -> ... throw:{someOtherError, X} -> ... end
同时,检测错误的代码也应该带有匹配的 throw,就像这样:
my_func(X) -> case ... of ... ... -> ... throw({thisError, ...}) ... -> ... throw({someOtherError, ...})
捕捉一切可能的异常错误
如果想要捕捉一切可能的错误,就可以使用下面的句式:
try Expr catch _:_ -> ... 处理所有异常错误的代码 ... end
如果在代码里漏写了标签:
try Expr catch _ -> ... 处理所有异常错误的代码 ... end
就不会捕捉到所有错误,因为在这种情形下系统会假设标签是默认的 throw。
栈跟踪
捕捉到一个异常错误后,可以调用 erlang:get_stacktrace() 来找到最近的栈跟踪信息。
demo1() -> [catcher(I) || I <- [1,2,3,4,5]]. demo2() -> [{I, (catch generate_exception(I))} || I <- [1,2,3,4,5]]. demo3() -> try generate_exception(5) catch error:X -> {X, erlang:get_stacktrace()} end. catcher(N) -> try generate_exception(N) of Val -> {N, nomal, Val} catch throw:X -> {N, caught, thrown, X}; exit:X -> {N, caught, exited, X}; error:X -> {N, caught, error, X} end. generate_exception(1) -> a; generate_exception(2) -> throw(a); generate_exception(3) -> exit(a); generate_exception(4) -> {‘EXIT‘, a}; generate_exception(5) -> error(a).
1> try_test:demo3().
{a,[{try_test,generate_exception,1,
[{file,"try_test.erl"},{line,30}]},
{try_test,demo3,0,[{file,"try_test.erl"},{line,11}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,673}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,641}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,626}]}]}
上面的跟踪信息展示了试图执行 try_test:demo3() 时发生了什么。它表明程序在 generate_exception/1 函数中崩溃。
栈跟踪还抱哈了当前函数如果执行成功会返回何处的信息。栈跟踪里的各个元组都是 {Mod, Func, Arity, Info} 这种形式。Mod、Func 和 Arity 指明了某个函数,Info 则包含了栈跟踪里这一项的文件名和行号。
如果某个函数在表达式序列中被调用,那么调用位置和函数将要返回的位置几乎是一样的。如果被调用的函数表达式序列的最后一个函数,那么此函数的调用位置信息不回保留在栈上。Erlang 回对这一类代码进行尾调用优化,因此栈跟踪信息不会记录被调时的位置,只会记录它将要返回的位置。
分析栈跟踪信息能让我们很好的判断出错误发生时程序的执行位置。通常栈跟踪信息的头两天就足以让你找到错误发生的位置了。
永远不要在函数被错误参数调用时返回一个值,而是要抛出一个异常错误。要假定调用者会修复这个错误。
抛出错误的原则
第一、应该在错误发生时立刻将它抛出,而且要抛的明显。错误消息应当被写入永久性的错误日志,而且要包含足够多的细节,以便过后查看是哪里出了错。
第二、只有程序员才应该看到程序崩溃时产生的详细错误消息。程序的用户绝对不能看到这些消息。