legoscia
legoscia

Reputation: 41618

Include function arguments in stack trace when raising error

In Erlang, when raising an error I can include the arguments to the current function by passing a second argument to error:

-module(foo).

-export([foo/2]).

foo(A, B) ->
    error(something_went_wrong, [A, B]).

This means that the arguments will be visible in the stack trace, which can make debugging easier:

> foo:foo(1, 2).
foo:foo(1, 2).
** exception error: something_went_wrong
     in function  foo:foo/2
        called as foo:foo(1,2)

In both Erlang and Elixir, this happens automatically on function clause errors:

defmodule Foo do
  def foo(a, b) when false do
    :ok
  end
end
> Foo.foo(1, 2)
** (FunctionClauseError) no function clause matching in Foo.foo/2    
    
    The following arguments were given to Foo.foo/2:
    
        # 1
        1
    
        # 2
        2
    
    foo.ex:2: Foo.foo/2

Is there a way to include function arguments for explicitly raised exceptions in Elixir, other than calling :erlang.error directly? I looked at the documentation for Kernel.raise and didn't find anything suitable.

Upvotes: 1

Views: 412

Answers (2)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121010

There is no generic way of doing that because the exception in is just a struct. On the other hand, that gives you the flexibility that is, for instance, used in the aforementioned FunctionClauseError, that is defined as

defexception [:module, :function, :arity, :kind, :args, :clauses]

So when the compiler raises it, message is constructed to include arguments passed through with the original error.


To achieve the same, one usually defines the custom exception with Kernel.defexception/1, accepting arguments and implementing both message/1 and blame/2 callbacks from Exception behaviour. A good example would be the source of FunctionClauseError implementation, I linked above.

Upvotes: 1

Everett
Everett

Reputation: 9618

It feels like a cop-out, but I believe the preferred way of doing this is to use inspect/2 on variables in your message, e.g.

raise ArgumentError, 
    message: "There was a problem with the inputs: #{inspect(a)} #{inspect(b)}"

# Yields:    
# ** (ArgumentError) There was a problem with the inputs: "foo" "bar"

The examples in the Elixir defexception docs more or less do the same, just attached to a custom exception module, something like:

defmodule ABError do
  defexception [:message]
end

a = "foo"
b = "bar"

raise ABError,
  message: "There was a problem with the inputs: #{inspect(a)} #{inspect(b)}"

# Yields:
# ** (ABError) There was a problem with the inputs: "foo" "bar"

With your background in Erlang, you might be able to make more sense of how all this threads its way back through the Exception module

Upvotes: 1

Related Questions