Skip to content

Exceptions and stack traces #1723

@yrashk

Description

@yrashk

I have a strong suspicion that we need to improve our exception raising with regards to stack traces. I am observing less than optimal behaviour with our current implementation.

Basically, I suspect that the root of the problem is in the fact that Kernel.raise is a set of functions, not macros, and they are ultimately creating a problem by messing up the stack.

Observe this:

defmodule X do

 defexception E, message: "x"

 def t do
   raise E
 end

end
iex(3)> X.t
** (X.E) x
    erl_eval.erl:569: :erl_eval.do_apply/6
    src/elixir.erl:138: :elixir.eval_forms/3

Nowhere here you can actually see where the raise is happening. I "discovered" this problem in the real system we're developing when I started paying more attention to logs — our logging shows this every day — and now we have to guess where a lot of errors come from. That's quite painful :)

I made a proof of concept patch for v0.10.2 (didn't have a chance to upgrade to master yet), and I am getting very positive results:

iex(2)> X.t
** (X.E) x
    iex:6: X.t/0
    erl_eval.erl:569: :erl_eval.do_apply/6
    src/elixir.erl:138: :elixir.eval_forms/3

As you can see, it actually properly invluces X.t/0 into the stacktrace.
Is there any interest to fix this? My patch is not perfect, but it was useful in illustrating the potential in fixing this problem.

P..S. My quick-n-dirty patch:

diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex
index 2cf8c29..e70c813 100644
--- a/lib/elixir/lib/kernel.ex
+++ b/lib/elixir/lib/kernel.ex
@@ -1,5 +1,3 @@
-import Kernel, except: [raise: 1, raise: 2]
-
 defmodule Kernel do
   @moduledoc """
   `Kernel` provides the default macros and functions
@@ -2015,8 +2013,7 @@ defmodule Kernel do

     case is_atom(expanded) do
       false ->
-        raise ArgumentError,
-          message: "invalid arguments for use, expected an atom or alias as argument"
+        :erlang.error ArgumentError.exception(message: "invalid arguments for use, expected an atom or alias as argument")
       true ->
         quote do
           require unquote(expanded)
@@ -2652,8 +2649,8 @@ defmodule Kernel do
     new_acc =
       case condition do
         { :_, _, atom } when is_atom(atom) ->
-          raise ArgumentError, message: <<"unbound variable _ inside cond. ",
-            "If you want the last clause to match, you probably meant to use true ->">>
+          :erlang.error ArgumentError.exception(message: <<"unbound variable _ inside cond. ",
+            "If you want the last clause to match, you probably meant to use true ->">>)
         x when is_atom(x) and not x in [false, nil] ->
           clause
         _ ->
@@ -3095,7 +3092,7 @@ defmodule Kernel do
   end

   defp pipeline_op(_, arg) do
-    raise ArgumentError, message: "unsupported expression in pipeline |> operator: #{Macro.to_string arg}"
+    :erlang.error ArgumentError.exception(message: "unsupported expression in pipeline |> operator: #{Macro.to_string arg}")
   end

   @doc """
@@ -3120,12 +3117,10 @@ defmodule Kernel do

   """
   @spec raise(binary | atom | tuple) :: no_return
-  def raise(msg) when is_binary(msg) do
-    :erlang.error RuntimeError[message: msg]
-  end
-
-  def raise(exception) do
-    raise(exception, [])
+  defmacro raise(msg) when is_binary(msg) do
+    quote do
+      :erlang.error RuntimeError[message: unquote(msg)]
+    end
   end

   @doc """
@@ -3146,8 +3141,15 @@ defmodule Kernel do

   """
   @spec raise(tuple | atom, list) :: no_return
-  def raise(exception, args) do
-    :erlang.error exception.exception(args)
+  defmacro raise(exception, args // []) do
+    quote do
+      case unquote(exception) do
+        e when is_binary(e) ->
+          :erlang.error RuntimeError.new(message: unquote(exception))
+        _ ->
+          :erlang.error unquote(exception).exception(unquote(args))
+      end
+    end
   end

   @doc """
@@ -3288,15 +3290,15 @@ defmodule Kernel do
                 { :error, _ } ->
                   :elixir_aliases.ensure_loaded(caller.line, caller.file, atom, caller.context_modules)
                 _ ->
-                  raise ArgumentError, message: "cannot use module #{inspect atom} in access protocol because it does not export __record__/1"
+                  :erlang.error ArgumentError.exception(message: "cannot use module #{inspect atom} in access protocol because it does not export __record__/1")
               end
           end

         Record.access(atom, fields, args, caller)
       false ->
         case caller.in_match? do
-          true  -> raise ArgumentError, message: << "the access protocol cannot be used inside match clauses ",
-                     "(for example, on the left hand side of a match or in function signatures)" >>
+          true  -> :erlang.error ArgumentError.exception(message: << "the access protocol cannot be used inside match clauses ",
+                     "(for example, on the left hand side of a match or in function signatures)" >>)
           false -> quote do: Access.access(unquote(element), unquote(args))
         end
     end
@@ -3347,14 +3349,14 @@ defmodule Kernel do
     funs = Macro.escape(funs, unquote: true)
     quote bind_quoted: [funs: funs, opts: opts] do
       target = Keyword.get(opts, :to) ||
-        raise(ArgumentError, message: "Expected to: to be given as argument")
+        :erlang.error ArgumentError.exception(message: "Expected to: to be given as argument")

       append_first = Keyword.get(opts, :append_first, false)

       lc fun inlist List.wrap(funs) do
         case Macro.extract_args(fun) do
           { name, args } -> :ok
-          :error -> raise ArgumentError, message: "invalid syntax in defdelegate #{Macro.to_string(fun)}"
+          :error -> :erlang.error ArgumentError.exception(message: "invalid syntax in defdelegate #{Macro.to_string(fun)}")
         end

         actual_args =
@@ -3616,7 +3618,7 @@ defmodule Kernel do
         IO.write "%w()b is deprecated, please use %w()s instead\n#{Exception.format_stacktrace}"
         ?s
       [mod] when mod in [?s, ?a, ?c] -> mod
-      _else -> raise ArgumentError, message: "modifier must be one of: s, a, c"
+      _else -> :erlang.error ArgumentError.exception(message: "modifier must be one of: s, a, c")
     end

     case is_binary(string) do
diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs
index cd10fea..e38ef34 100644
--- a/lib/elixir/test/elixir/exception_test.exs
+++ b/lib/elixir/test/elixir/exception_test.exs
@@ -84,6 +84,19 @@ defmodule Kernel.ExceptionTest do
     assert ErlangError.new(original: :sample).message == "erlang error: :sample"
   end

+  test :raise_preserves_the_stacktrace do
+    stacktrace =
+    try do
+      raise "a"
+    rescue _ -> 
+      [top|_] = System.stacktrace
+      top
+    end
+    file = to_char_list(__FILE__)
+    assert {Kernel.ExceptionTest, :test_raise_preserves_the_stacktrace, _,
+           [file: ^file, line: 90]} = stacktrace # line #90 is sensitive
+  end
+
   defp empty_tuple, do: {}
   defp a_tuple, do: { :foo, :bar, :baz }
   defp a_list,  do: [ :foo, :bar, :baz ]

Tests pass. I can send a PR if necessary

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions