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

EEP 47: Add syntax in try/catch to retrieve the stacktrace directly

Abstract

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.

Specification

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.

Motivation

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.

Rationale

The Syntax

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.

Why Not Allow Matching On 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.

Limiting The Scope Of erlang:get_stacktrace/0 Instead?

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).

Backwards Compatibility

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.

Implementation

The implementation can be found in PR #1634.

Copyright

This document has been placed in the public domain.