作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
狀態
最終版/24.0 已在 OTP 版本 24 中實作
類型
標準追蹤
建立日期
2020-09-14
Erlang 版本
OTP-24.0
發布歷史
2020-10-14, 2020-11-27

EEP 54:提供更多關於錯誤的資訊 #

摘要 #

此 EEP 提出一種機制,用於報告當 BIF 引發例外時,更多人類可讀的資訊來說明發生了什麼錯誤。函式庫或應用程式可以使用相同的機制來提供更詳細的錯誤訊息。

規範 #

在 OTP 23 和更早的版本中,當對內建函式 (BIF) 的呼叫失敗時,shell 會印出簡短的訊息

1> element(a,b).
** exception error: bad argument
     in function  element/2
        called as element(a,b)

bad argument 訊息告訴我們,呼叫的一個或多個引數在某些方面不正確(在此範例中,兩個引數的類型都錯誤)。

我們提出一種機制,使 shell 能夠印出更有用的錯誤訊息。以下是此 EEP 的參考實作會如何印出訊息

1> element(a, b).
** exception error: bad argument
     in function  element/2
        called as element(a,b)
        *** argument 1: not an integer
        *** argument 2: not a tuple

請注意,訊息的確切格式和措辭是此 EEP 範圍之外的實作細節。此處將指定的是使這些訊息成為可能的 API 和慣例。

此 EEP 中的提案 #

  • 擴展呼叫堆疊回溯 (stacktrace) 格式的格式,以指示該呼叫存在擴展的錯誤資訊,以及如何提供擴展的錯誤資訊的慣例。

  • 新的 erlang:error/3 BIF,允許函式庫和應用程式在堆疊回溯中引發具有擴展錯誤資訊的例外。

  • 新的函式 erl_error:format_exception/3erl_error:format_exception/4,允許函式庫和應用程式以與 shell 相同的樣式格式化堆疊回溯。

擴展堆疊回溯 #

堆疊回溯 (stacktrace) 目前是元組的列表。就此 EEP 而言,我們只對堆疊回溯中的第一個項目感興趣。它的外觀為 {Module,Function,Arguments,ExtraInfo},其中 ExtraInfo 是雙元組的列表。為了表示擴展的錯誤資訊可用,我們建議在堆疊回溯的第一個元素中將 {error_info,ErrorInfoMap} 元組新增至 ExtraInfo

此 map ErrorInfoMap 可能包含有關錯誤的其他資訊或有關如何處理錯誤的提示。

目前,有三個選用的鍵具有定義的含義

  • module 的值是一個模組名稱,可以呼叫該模組以提供有關錯誤的其他資訊。預設值為堆疊回溯項目中的 Module

  • function 的值是在提供錯誤資訊的模組中要呼叫的函式名稱。預設名稱為 format_error

  • cause 的值(如果存在)提供有關錯誤的其他資訊。

若要取得有關錯誤的更多資訊,可以呼叫由 modulefunction 鍵的值命名的函式。在本文檔的後續內容中,為了簡潔起見,我們將呼叫該函式為 format_error/2

format_error/2 的引數是例外原因(對於 BIF 呼叫,通常為 badarg)和堆疊回溯。

因此,如果對 element/2 的呼叫失敗,出現 badarg 例外,並且堆疊回溯中的第一個項目為

{erlang,element,[1,no_tuple],[{error_info,ErrorInfoMap}]}

並假設堆疊回溯已繫結至變數 StackTrace,則下列呼叫將提供有關錯誤的其他資訊

FormatModule = maps:get(module, ErrorInfoMap, erlang),
FormatFunction = maps:get(function, ErrorInfoMap, format_error),
FormatModule:FormatError(badarg, StackTrace)

format_error/2 函式應傳回一個 map。對於每個發生錯誤的引數,都應該有一個 map 元素,其鍵為引數編號(即,第一個引數為 1,第二個引數為 2,依此類推),值為 unicode:chardata() 項。

原子 generalreason 也可以在 map 中傳回。general 表示不歸因於特定引數的通用錯誤(例如,當預設裝置停止時的 io:format("Hello") 的 badarg)。reason 將告知錯誤美化列印器印出傳回的字串,而不是錯誤原因。generalreason 指向的值應為 unicode:chardata() 項。

當呼叫 format_error/2 函式時,您可以修改第一個堆疊回溯項目的 error_info map,以新增一個選用的 pretty_printer 鍵,其值為 arity 1 的匿名函式。format_error/2 實作可以使用任何 Erlang 項呼叫匿名函式,而且它必須傳回 unicode:chardata() 項,其中包含給定項的美化列印表示法。

例如

Args = [1,no_tuple],
StackTrace = [{erlang, element, Args, [{error_info,Map}]}],
erlang:format_error(badarg, StackTrace)

可以傳回

#{2 => <<"not a tuple">>}

而且

Args = [0, b],
StackTrace = [{erlang, element, Args, [{error_info,Map}]}],
erlang:format_error(badarg, Entry)

可以傳回

#{1 => <<"out of range">>, 2 => <<"not a tuple">>}

而且

Args = ["Hello"],
StackTrace = [{io, format, Args, [{error_info,Map}]}],
erlang:format_error(badarg, Entry)

可以傳回

#{general => "the device has terminated"}

請注意,ErrorInfoMap 項中鍵 cause 的值(如果存在)僅供 format_error/2 使用。特定錯誤的實際值可能隨時變更。

cause 鍵通常只會在 BIF 中發生錯誤時才會存在,而該錯誤取決於執行階段系統中的內部狀態(例如 register/2 或 ETS BIF),或是對於具有複雜引數的 BIF(例如 system_flag/2),這會讓找出哪個引數發生錯誤變得繁瑣且容易出錯。

以下是 erlang 模組的 format_error/2 的一種實作方式

format_error(ExceptionReason, [{erlang, F, As, Info} | _]) ->
    ErrorInfoMap = proplists:get_value(error_info, Info, #{}),
    Cause = maps:get(cause, ErrorInfoMap, none),
    do_format_error(F, As, ExceptionReason, Cause).

do_format_error(_, _, system_limit, _) ->
    %% The explanation for system_limit is clear enough, so we don't
    %% need any detailed explanations for the arguments.
    #{};
do_format_error(F, As, _, Cause) ->
    do_format_error(F, As, Cause).

do_format_error(element, [Index, Tuple], _) ->
    Arg1 = if
               not is_integer(Index) ->
                   <<"not an integer">>;
               Index =< 0; Index > tuple_size(Tuple) ->
                   <<"out of range">>;
               true ->
                   []
           end,
    Arg2 = if
               not is_tuple(Tuple) -> <<"not a tuple">>;
               true -> []
           end,
    PotentialErrors = [{1, Arg1}, {2, Arg2}],
    maps:from_list([{ArgNum, Err} ||
                       {ArgNum, Err} <- PotentialErrors,
                       Err =/= []]);

do_format_error(list_to_atom, _, _) ->
    #{1 => <<"not a flat list of characters">>};

do_format_error(register, [Name,PidOrPort], Cause) ->
    [Arg1, Arg2] =
    case Cause of
        registered_name ->
            [[],<<"this process or port already has a name">>];
        notalive ->
            [[],<<"the pid does not refer to an existing process">>];
        _ ->
            Errors =
                [if
                     Name =:= undefined -> <<"'undefined' is not a valid name">>;
                     is_atom(Name) -> [];
                     true -> <<"not an atom">>
                 end,
                 if
                     is_pid(PidOrPort) -> [];
                     is_port(PidOrPort) -> [];
                     true -> <<"not a pid or a port">>
                 end],
            case Errors of
                [[],[]] ->
                    [<<"name is in use">>];
                [_,_] ->
                    Errors
            end,
    PotentialErrors = [{1, Arg1}, {2, Arg2}],
    maps:from_list([{ArgNum, Err} ||
                       {ArgNum, Err} <- PotentialErrors,
                       Err =/= []]);
      .
      .
      .

do_format_error(_, _, _) ->
    #{}.

請注意,針對不同的 BIF 使用不同的策略來判斷擴展的錯誤資訊

  • 首先,處理 system_limit 例外(無論呼叫的 BIF 為何)。不傳回擴展的錯誤資訊,因為 system_limit 的說明已足夠清楚。

  • 如果 element/2 失敗,format_error/2 函式只會檢查 element/2 的引數。

  • 如果 list_to_atom/1 引發 badarg 例外,則只有一個可能的錯誤原因,因此無需檢查引數。

  • 如果 register/2 BIF 失敗,對應於 cause 鍵的值會針對兩個可能失敗原因提供特定的錯誤原因。如果原因不是其中之一,format_error/2 將根據引數找出其他原因。

使用 erlang:error/3 提供擴展的錯誤資訊 #

函式庫或應用程式可以透過呼叫 erlang:error(Reason, Arguments, Options) 來引發具有擴展錯誤資訊的錯誤例外。Reason 應該是錯誤原因(例如 badarg),Arguments 應該是呼叫函式的引數,而 Options 應該是 [{error_info,ErrorInfoMap}]

erlang:error/3 的呼叫者應提供一個 format_error/2 函式(如果 ErrorInfoMap 具有 function 鍵,則不一定要使用該名稱),其行為如前一節所述。

格式化堆疊回溯 #

為了讓應用程式和函式庫能夠以與 shell 相同的樣式格式化堆疊回溯,因此提供了 erl_error:format_exception/3erl_error:format_exception/4 函式。以下是如何使用 erl_error:format_exception/3 的範例

try
    .
    .
    .
catch
    C:R:Stk ->
        Message = erl_error:format_exception(C, R, Stk),
        io:format(LogFile, "~ts\n", [Message])
end.

erl_error:format_exception/4 函式類似,但有第四個選項引數來支援自訂訊息。請參閱參考實作中的文件以取得詳細資訊。

未來可能的擴展 #

由於堆疊回溯中的 error_info 元組包含一個 map,因此可以在此 EEP 未來的擴展中,將更多資料新增至此 map。

同樣地,由於 format_error/2 的傳回值是一個 map,因此可以在未來將 map 中的其他鍵指定一個含義。

例如,鍵 hint 的值可以是較長的訊息,提供更多上下文,或提供關於如何調查或避免錯誤的具體建議。

其他範例 #

讓我們看看一些使用 ETS 的範例

1> T = ets:new(table, []).
#Ref<0.2290824696.4161404930.5168>
2> ets:update_counter(T, k, 1).
** exception error: bad argument
     in function  ets:update_counter/3
        called as ets:update_counter(#Ref<0.2290824696.4161404930.5168>,k,1)
        *** argument 2: not a key that exists in the table

請注意,當評估在 shell 中輸入的運算式時發生錯誤時,評估器處理程序會終止,且該處理程序建立的所有 ETS 資料表都會遭到刪除。因此,使用相同的引數再次呼叫 update_counter 會產生不同的訊息

3> ets:update_counter(T, k, 1).
** exception error: bad argument
     in function  ets:update_counter/3
        called as ets:update_counter(#Ref<0.2290824696.4161404930.5168>,k,1)
        *** argument 1: the table identifier does not refer to an existing ETS table

重新開始,建立新的 ETS 資料表

4> f(T), T = ets:new(table, []).
#Ref<0.2290824696.4161404930.5205>
5> ets:insert(T, {k,a,0}).
true
6> ets:update_counter(T, k, 1).
** exception error: bad argument
     in function  ets:update_counter/3
        called as ets:update_counter(#Ref<0.2290824696.4161404930.5205>,k,1)
        *** argument 3: the value in the given position in the object is not an integer
7> ets:update_counter(T, k, bad).
** exception error: bad argument
     in function  ets:update_counter/3
        called as ets:update_counter(#Ref<0.2290824696.4161404930.5205>,k,bad)
        *** argument 1: the table identifier does not refer to an existing ETS table
        *** argument 3: not a valid update operation

動機 #

當對 BIF 的呼叫失敗,原因為 badarg 時,即使是經驗豐富的開發人員,也並不總是能清楚地知道哪個引數「錯誤」以及錯誤的方式。對於新手來說,必須弄清楚 badarg 的含義是掌握新語言的另一個障礙。

即使是經驗豐富的開發人員,也很難或無法弄清楚某些 BIF 的 badarg 例外的原因。例如,撰寫時 ets:update_counter/4 的文件 列出了 ets:update_counter/4 會失敗的 8 種情況。這個數字太低了。例如,遺漏在清單中的原因是 ETS 資料表遭到刪除或存取權限不足等原因。

新增了 general 傳回鍵,以便提供有關 io:format("hello") 中預設 I/O 裝置的資訊。它也讓協力廠商 error_report 實作(例如 Elixir)在可以傳回的內容方面有更大的自由度。

新增了 reason 傳回鍵,以便協力廠商 error_report 實作(例如 Elixir)影響列印哪些內容來描述實際錯誤。

基本原理 #

為何不將 badarg 變更為更具資訊性的內容? #

提供更多有關錯誤的資訊的另一種方法是引入其他例外原因。例如,呼叫

element(a, b)

可能會引發例外

{badarg,[{1,not_integer},{2,not_tuple}]}

此變更可能會破壞預期 BIF 應引發 badarg 例外的程式碼。現有程式碼比對堆疊回溯中的第四個項目的可能性較低。

另一個相關的原因是,修改所有內建函數的錯誤處理程式碼需要大量的工作。在 C 語言中實作 Erlang 術語的建構既繁瑣又容易出錯。當發生錯誤時,該程式碼中的錯誤總是有可能導致執行時系統崩潰。測試套件必須極其詳盡,以確保找出所有錯誤,因為錯誤處理程式碼通常不常執行。

為什麼堆疊追蹤不能包含完整的錯誤原因?#

我們確實考慮過修改所有 BIF 的實作,以便它們在失敗時在堆疊追蹤中產生完整的錯誤資訊。然而,如前所述,在 C 語言中建構 Erlang 術語既繁瑣又容易出錯。

透過我們採用的方法,讓 Erlang 程式碼執行大部分錯誤原因的分析,可以大大降低錯誤處理導致應用程式或執行時系統崩潰的風險。

為什麼 map 的鍵值名稱是 cause 而不是 reason#

為了避免與例外狀況的原因混淆。

為什麼 ErrorInfoMapcause 的值沒有文件說明?#

ErrorInfoMap 中的原因並非旨在用於以程式方式找出錯誤發生的原因,而僅供 Module:format_error/2 產生人類可讀的訊息。

此外,對於許多 BIF,cause 鍵值將不存在,因為 Module:format/4 函數將僅根據 BIF 的名稱及其引數產生訊息。

module 鍵值有什麼用?#

  • 在 OTP 中,所有 format_error/2 函數都會位於與實作模組不同的模組中,以便於在儲存空間有限的系統中縮減 OTP 的大小。擁有 module 鍵值可以避免在實作模組中擁有重新導向的 format_error/2 函數的需求。

  • 程式庫或應用程式可能希望有一個單一模組為多個模組實作 format_error/2。例如,在 OTP 中,我們可能會有 erl_stdlib_errors 模組為 binaryetslistsmapsstringunicode 模組實作 format_error/2

function 鍵值有什麼用?#

  • 模組可能已經有一個名為 format_error/2 的函數。

  • 未來我們可能希望擴充編譯器,以產生它自己的 format_error/2 錯誤函數,以提供更多關於 badmatchfunction_clause 錯誤的資訊。

向後相容性 #

來自 BIF 的所有例外狀況現在都會在呼叫堆疊回溯(堆疊追蹤)中具有 ExtraInfo 元素(在 OTP 23 的文件中稱為 Location),其中包括 error_info 元組。在先前的版本中,失敗的 BIF 呼叫的 ExtraInfo 元素會是一個空列表。

明確對堆疊追蹤進行比對並對 ExtraInfo 元素的版面配置做出假設(例如,假設 Location 是空列表或特定順序的 fileline 元組列表)的應用程式可能需要修改。請注意,這種假設從來都不是安全的,並且錯誤處理的文件強烈建議開發人員不要依賴堆疊追蹤條目進行除錯以外的目的。

實作 #

參考實作包括在 erlangets 模組中以 C 語言實作的大多數 BIF 的擴充錯誤資訊。可以在 PR #2849 中找到它。

版權 #

本文檔已置於公有領域。