Help an Elixir beginner find missing development tooling!

Huh?

iex(1)> a = 42
42
iex(2)> try do 
...(2)>   throw :ack
...(2)> catch v ->
...(2)>   b = 6.28
...(2)>   IO.inspect(binding(), label: :Bindings)
...(2)> end
Bindings: [a: 42, b: 6.28, v: :ack]
[a: 42, b: 6.28, v: :ack]

If a binding ‘can’ be used in the local scope, then binding() will show it, it just compiles to a keyword list of bindings names to the bindings themselves, like [a: a, b: b, v: v]. Is there a case where it will not show a binding that ‘can’ in fact be accessed in the local scope?!

2 Likes

Thanks for this tip! Do you happen to remember what version this was in? I spot checked a couple of versions through 1.3 and didn’t see anything.

Ah! Interesting. Is it possible to retrieve a context for the binding call from System.stacktrace? If so, then we could plug it (ha! no pun intended!) into the Plug.Debugger to retrieve bindings for each level of the stack trace. The issue here is the bindings for Plug.Debugger won’t be helpful, we need bindings up the stack.

That information is already available at compile time via macro’s, so a macro can already access that information.

Hmm?

Yeah, but in general you cannot do:

def call(conn, opts) do
  a = 10
  throw :ack
end

try do
  call(conn, [])
catch
  v ->
    IO.inspect(binding(__STACKTRACE__)) # or other magic there
end

And get value of a. And that is what would be needed for what @mike_bianco want.

1 Like

What a? There is no a in scope at the point binding is called? Is there supposed to be an a?

In debuggers for many languages it’s possible to “break on exception” where the debugger would attach immediately where an exception happened (not anywhere higher up the stack). This is similar to what Ruby’s pry-rescue does and this is what @mike_bianco wants.

I’m not sure if it’s doable on the BEAM but I’d love to see something like that (preferably using the :debugger instead of IEx but I know many people would prefer the pry)

1 Like

Exactly. It’s an incredibly helpful debugging tool in ruby, and I would love to see something like this on the elixir side!

Offtopic: This quote made my day, thank you so much! I am to use it to point out any architecture/design/performance/database/younameit issue from now on. Like in CR, when there is, say, an if on function argument instead of different pattern matching function clauses:

Is there supposed to be an a?

And even in the real life, talking to my wife; it is applicable literally everywhere.

1 Like

It should present value of a form the place where exception was thrown. AFAIK it is not possible at all in Erlang, but this is how better-errors does it - it allows you to check values of local variables in place where exception was raised.

Ah, not sure I’ve ever used a language like that outside of stackless python. In C++/OCaml/Rust/Etc… once the stack frame is unwound anything that was existing before is now gone (although with some unsafe pointing munging you can sometimes get some old values, but don’t rely on that at all).

I’m not even sure how that is safe to do without heap-based frames. I’m guessing Ruby uses heap-based frames. The BEAM’s actors use stack-based frames so it is not safe to do such a mapping.

Except on the BEAM by the time the exception is unwound that stack frame is already gone and potentially even GC’d.

Except that stack frame is gone. If you want bindings from the thrown site then they need to be packaged up into the exception itself, which binding() would be great to use for, maybe make a library that has a dthrow/draise for a decorated throw and raise along with a dtry for a decorated exception handler for them? :slight_smile:

What’s better-errors? Link please? I’m quite curious. ^.^

It’s possible in Java (at least in IntelliJ IDE), C++ (both VS debugger and gdb), C# and Python, so I’m pretty sure you used a language like that.

Those debuggers specifically overwrite the exception thrower with their own code (python adds a trace handler to the call, python’s debugging is actually very similar to the beam’s but at a far more fine-grained level) so as to be able to debug at the point of throwing instead of in the handler. Once you have unwound to the handler though, the information is gone.

So to have such debugger for Elixir it would require a modified version of the BEAM?

Not necessarily, rather a library to decorate the throw/raise/try calls with extra information, though you’d have to remember to use it in each file when necessary.

That is why I am saying that such library cannot exist in Elixir world, as there is just not enough data :wink:

It is library for Rails that provide better error pages on Rails exceptions in development.

And it still would provide less informations as it would not handle errors in dependencies. Also you cannot overwrite try in Elixir at all (it is special form).

Ah, never used Rails or Ruby, only better_errors library I’d heard of was for OCaml. ^.^;

Well you could but you’d have to apply a preprocessor to the files before feeding them into the compiler, doable but would require a mix.exs line added and a lot of work in the library.

But a macro that processes the entire module can modify it.

But a macro that processes the entire module can modify it.

Would it be possible to write a macro which processes every module loaded to override try to include this debugging information?

I still find this one of the most parts of ruby I miss once. For instance, if I’m running a test and it’s failing due to a function deep in the stack I would love to know what arguments were passed to that function so I can quickly debug what might be happening higher in the stack.

In ruby, I can just inspect the args and quickly modify the code. In elixir I have to add pry to the right place in the stack that I control, rerun the tests, and hope that I placed the debugger in the correct spot.

I think adding this feature to elixir (in dev/test mode only) would be amazing and be an incredible help to users.

Just use erlang:trace/3 or dbg. Due to the nature of Erlang the frame of the failed code do not exists anymore, so you cannot get any more information that is showed by the VM, you need to fall back to other features - tracing is one of them. This can be shown for example in form of:

Task.async(fn -> 1 + :a end)

It has no information from where it was called at all, so you will have no information where the error came from other than it come from the new process.

@hauleth Where can I find more information about using trace/dbg? How can I can debug a piece of code I don’t own (pulled in by mix deps)? Thanks!

In the Erlang docs. Yes, you can trace (almost) all Erlang function calls (some BIFs aren’t traceable for performance reasons, but there is only few of them).

1 Like