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:

Why Not Allow Matching On The Stacktrace?

Stacktrace must be a variable, not a pattern. There are two reasons:

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:

Implementation

The implementation can be found in PR #1634.

Copyright

This document has been placed in the public domain.