Author: Björn Gustavsson <bjorn(at)erlang(dot)org>
Status: Final/21.0 Proposal is to be included in OTP release 21.0
Type: Standards Track
Created: 23-Nov-2017
Erlang-Version: 21
Post-History: 24-Nov-2017, 30-Nov-2017
This EEP proposes an extension to the try/catch
statement to allow
the call stack back-trace (stacktrace) to be retrieved without
calling erlang:get_stacktrace/0
.
We will introduce new syntax to retrieve the call stack back-trace
(hereafter called stacktrace). Currently,
erlang:get_stacktrace/0
can be called at any time to retrieve the
stacktrace from the last exception that occurred in the current
process.
The current syntax for try/catch
is:
try
Exprs
catch
[Class1:]ExceptionPattern1 [when ExceptionGuardSeq1] ->
ExceptionBody1;
[ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
We propose the following extension of the syntax for exception clause heads:
Class:ExceptionPattern:Stacktrace [when ExceptionGuardSeq] ->
Stacktrace
must be a variable name, not a pattern. Furthermore,
Stacktrace
must not be previously bound and it must not be
referenced in ExceptionGuardSeq
.
Here is an example:
try
Exprs
catch
something_was_thrown ->
%% The default class is 'throw'.
.
.
.
throw:something_else_was_thrown ->
.
.
.
throw:thrown_with_interesting_stacktrace:Stk ->
%% The class 'throw' must be explicitly given when
%% the stacktrace is to be retrieved.
.
.
.
error:undef ->
%% Handle an undefined function specially.
.
.
.
C:E:Stk ->
%% Log any other exception and rethrow it.
log_exception(C, E, Stk),
raise(C, E, Stk)
end.
The main motivation for this feature is to be able to deprecate
(and ultimately remove) erlang:get_stacktrace/0
.
The problem with erlang:get_stacktrace/0
is that it forces the
stacktrace from the latest exception in a process to be retained
until another exception occurs or the process terminates. The
stacktrace often includes the arguments for the last function
call, BIF call, or (in OTP 21) operator that failed. The arguments
can be of any size.
Here is an example:
1> catch abs(lists:seq(1, 1000)).
{'EXIT',{badarg,
[{erlang,abs,
[[1,2,3,4,5,6,7,8,9,10,11,12,13,
14,15,16,17,18,19,20|...]],
[]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
{erl_eval,expr,5,[{file,"erl_eval.erl"},{line,431}]},
{shell,exprs,7,[{file,"shell.erl"},{line,687}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]}}
2>
The list containing the integers from 1 through 1000 will be kept in the process that caused the exception until another exception occurs in the same process.
In a future release, where erlang:get_stacktrace/0
has either been
changed to always return []
or been removed, it is no longer
necessary to keep the stacktrace in the process indefinitely.
Another motivation is that pitfalls such as the one in the following example are impossible:
try
Expr
catch
C:E ->
do_something(),
log_exception(C, E, erlang:get_stacktrace())
end
If do_something()
generates and catches an exception, the call
to erlang:get_stacktrace/0
will retrieve the wrong stacktrace.
Regarding the syntax, we did consider using another token instead of
colon before the stacktrace variable. That would continue to allow
making the class throw
implicit even when retrieving the stacktrace.
That is, you could write an exception pattern, followed by some
special token, followed by the name of the stacktrace variable, and
the class throw
would be implicitly understood.
We rejected that for two reasons:
We could not find a suitable separator token. Our best suggestion
was @
, and that will not work because @
is allowed in atoms.
Tokens like /
could confuse at least the parser (and possibly human
readers) because patterns are allowed to contain constant expressions.
A double colon (::
) would not cause any ambiguitiy issues, but
everyone immediately associated it with a type declaration.
In practice, when catching an exception of class throw
, one is
almost never interested in the stacktrace.
Stacktrace
must be a variable, not a pattern. There are two
reasons:
In general, pattern matching on the stacktrace is discouraged. The intention is that it should be inspected by a human to aid in debugging.
Allowing pattern matching on the stacktrace would be expensive. When an exception occurs, a raw stacktrace is saved. The raw stacktrace contains a limited number of continuation pointers (by default 8) collected from the stack and possibly the arguments for the function call or BIF call that failed. To convert the raw stacktrace to the symbolic form that can be matched or shown is quite expensive; by only allowing a variable, that conversion will only happen when a clause has matched and its body is about to be executed.
In OTP 20, we introduced a new warning in the documentation for
erlang:get_stacktrace/0
:
erlang:get_stacktrace/0
is only guaranteed to return a stacktrace if called (directly or indirectly) from within the scope of a try expression.
Our intention was that by limiting the scope, the stacktrace could be
cleared when exiting the scope. For example, the following code would
continue to work, but the stacktrace would be cleared when leaving
try/catch
:
try Expr
catch
C:R ->
{C,R,helper()}
end
helper() ->
erlang:get_stacktrace().
Unfortunately, the following slightly different example would force a hard choice upon us:
try Expr
catch
C:R ->
helper(C, R)
end
helper(C, R) ->
{C,R,erlang:get_stacktrace()}.
The call to helper/2
is tail-recursive. If we are to keep the call
tail-recursive, we cannot clear the stacktrace. Conversely, if we are
to clear the stacktrace, the call can no longer be tail-recursive.
Another problem is that the compiler cannot warn for all instances of
calls to erlang:get_stacktrace/0
that would not return a stacktrace.
All it can do is to warn for obvious calls that will not work such as
in the following example:
try Expr
catch
C:R ->
.
.
.
end,
Stk = erlang:get_stacktrace(),
.
.
.
That is, the compiler can only warn if there is a use of try/catch
or catch
followed by a call to erlang:get_stacktrace/0
in the same
function.
We could limit the useful scope of erlang:get_stacktrace/0
to
just the syntactic scope of the clause within the try/catch
.
For example:
try
Expr
catch
C:E ->
Stk = erlang:get_stacktrace(),
log_exception(C, E, Stk)
end
It does not seem to be any advantage of that solution compared
to introducing the new syntax. Developers would still have to
update their programs (eliminating calls to erlang:get_stacktrace/0
from helper functions and moving them into the syntactic scope of
the try/catch
).
Since the new syntax would cause a compilation error in OTP 20 and previous releases, no existing source code can be affected.
The abstract format already includes a variable (with the name _
)
for the stacktrace in exception clauses. That means that many
tools that manipulate the abstract Erlang code will continue to
work without any change.
The erlang:get_stacktrace/0
BIF can be deprecated in several
stages to minimize the impact of the change.
For example:
In OTP 21, there will be a compiler warning that
erlang:get_stacktrace/0
is deprecated.
In OTP 23 (or possibly OTP 22), erlang:get_stacktrace/0
will start
returning []
. Many programs that have not been updated will
continue to work, except that if an exception is raised no stacktrace
will be available to aid in debugging.
In some future release (OTP 42?), erlang:get_stacktrace/0
can be
removed.
The implementation can be found in PR #1634.
This document has been placed in the public domain.