Three Ways to Debug Code in Elixir

pulkit110

Pulkit Goyal

Posted on December 7, 2021

Three Ways to Debug Code in Elixir

Elixir provides a very powerful suite of tools that devs can use to observe the behavior of their code and debug errors.

There are several different strategies you can use to debug code in Elixir.

While it is hard to produce a comprehensive list of all possible debugging methods, we will cover some of the most common methods in today's post.

1. The IO Module: puts/2 and inspect/2

Use the IO module for a quick and easy way to get some basic visibility into your code when debugging.

You can print out log statements that:

  • tell you where you are in executing code
  • inspect structs and other entities
  • display the function's arguments

puts/2 and inspect/2 are the most interesting to use when debugging. With these, it's easy to sprinkle a few good output messages throughout your code and then visualize what's happening.

puts/2 just prints out a string to the intended device (or :stdio if you don't provide anything).

inspect/2 does something similar but writes out formatted output (e.g., pretty-printing maps, structs, and arrays).
There are a couple of options to select the width or a label for the message:

> IO.inspect(%{foo: :bar}, label: "some map")
# some map: %{foo: :bar}
Enter fullscreen mode Exit fullscreen mode

The great thing about IO.inspect/2 is that it returns the input, so it is easy to tap into long pipes:

[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum
Enter fullscreen mode Exit fullscreen mode

In addition to this, if you need to create strings with embedded maps for the log messages, it is also possible to use Kernel.inspect/2 inside puts strings — like this:

> IO.puts("some map: #{inspect(%{foo: :bar})}")
# some map: %{foo: :bar}
Enter fullscreen mode Exit fullscreen mode

The inspect method has a lot more options to customize your output.

Finally, if you need to access a function's arguments quickly, it is possible to use binding/1.

defmodule Greeter do
  def greet(name \\ "John Doe") do
    IO.inspect(binding())
  end
end

Greeter.greet() # Prints [name: "John Doe"]
Greeter.greet("Jane Doe") # Prints [name: "Jane Doe"]
Enter fullscreen mode Exit fullscreen mode

2. IEx for Advanced Debugging Control

If you need advanced control for debugging, the next tool you'll find useful is the interactive shell IEx. IEx lets you inspect and visualize the current state of your code, manually execute code, and examine the results.

Just pop in require IEx; IEx.pry anywhere in your code and then run it with iex. So, if you are running a:

  • Standalone elixir file with elixir fib.exs, use iex -r fib.exs instead.
  • Mix command like mix run fib.exs or mix phx.server, use iex -S mix run fib.exs or iex -S mix phx.server.

Let's take a look at some of the things we can do with IEx.

Using Pry with IEx

Let's see how debugging works with pry by looking at a buggy Fibonacci number generator. The code does not produce the expected result. Let's put a require IEx; IEx.pry on line 9 (just after fib2 = fib) and run it with iex -r fib.exs:

Request to pry #PID<0.104.0> at Fib.number/1 (fib.exs:9)

    7:       fib1 = fib2
    8:       fib2 = fib
    9:       require IEx; IEx.pry
   10:     end)
   11:     fib2

Allow? [Yn] y
Interactive Elixir (1.12.0) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> binding()
[_i: 2, fib: 1, fib1: 1, fib2: 1, n: 5]
Enter fullscreen mode Exit fullscreen mode

We can see that it stops at the IEx.pry() call. Then, we can inspect the values of the variables (or just use binding to output all context).

For the first iteration, everything looks good, fib2 updates to 1, and fib1 uses the previous value of fib2, i.e., 1.

On the next iteration, we expect fib2 to be 1 + 1 = 2 and fib1 to be 1, and then fib2 to be 2 + 1 = 3 and fib1 to be 2, and so on.

So let's type continue to go to the next pry call and inspect the binding again:

pry(2)> continue
Break reached: Fib.number/1 (fib.exs:9)

    7:       fib1 = fib2
    8:       fib2 = fib
    9:       require IEx; IEx.pry
   10:     end)
   11:     fib2

pry(1)> binding()
[_i: 3, fib: 1, fib1: 1, fib2: 1, n: 5]
Enter fullscreen mode Exit fullscreen mode

Here, we see that we are on the next iteration (_i is 3), but apparently, the other variables do not change at all.
So this is where our bug lies. Everything is immutable in Elixir, so assigning variables inside the anonymous function creates new variables rather than overriding the ones on the outer scope.

We can now use this insight to fix our code.

If you are curious, see the fixed version of the Fibonacci number generator.

Using Breakpoints with IEx

In the previous section, we had to change the code to enter the pry session.
IEx also provides a break! function to set breakpoints without changing code.

This is very important when you want to set breakpoints in parts of code that you don't own, coming from a library or even from Elixir standard modules.

The only drawback is that this only works on compiled code, and you can only break! at the start of the function, not on any arbitrary line.

To use break! with our Fibonacci example:

$ elixirc fib.ex # This generates a beam file in your current dir
$ ls *.beam
Elixir.Fib.beam
$ iex            # This will load all beam files in the current directory
iex> IEx.Helpers.break!(Fib.number/1)
iex> Fib.number(5)
Break reached: Fib.number/1 (fib.ex:2)

    1: defmodule Fib do
    2:   def number(n) do
    3:     fib1 = 0
    4:     fib2 = 1
Enter fullscreen mode Exit fullscreen mode

Bonus: IEx Tips and Tricks

While we are on IEx, let us look at some general tips that can help you be more productive with it.

The first, and possibly the most important, is to enable shell history if you use IEx a lot. You can then press to get your last used commands or use ^ + R to reverse search the history of used commands.

There are two ways you can enable shell history:

  1. Enable each session by starting it with a flag:

    $ iex --erl "-kernel shell_history enabled"
    
  2. Enable all sessions by setting the ERL_AFLAGS environment on your shell. Depending on your terminal configuration, you will need to add the following (or its equivalent) to a startup script (like ~/.zshrc/~/.bashrc):

    export ERL_AFLAGS="-kernel shell_history enabled"
    

The second tip, which works on the recent version of Elixir (1.12+), means that you can use multi-line pipes directly in the shell. The pipe automatically gets the last evaluated statement's return value.

So you can just copy and paste long pipes from your code directly in the IEx session:

iex(1)> [1, [2], 3]
[1, [2], 3]
iex(2)> |> List.flatten()
[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

If you often use modules in IEx, you can create a file called .iex.exs from the directory used to access IEx. Alternatively, you can create a global file inside the home directory (~/.iex/exs), and it will be evaluated every time you open an IEx session.

# Load another ".iex.exs" file
import_file("~/.iex.exs")

# Import some module from lib that may not yet have been defined
import_if_available(MyApp.Mod)

# Import Ecto.Query so that querying is always available in the shell
import Ecto.Query
Enter fullscreen mode Exit fullscreen mode

Finally, there are some cases when you might be using IEx and you make a typo (like an additional bracket or ") and the command cannot be terminated, for example:

iex> ["abc"
...  "
...  ]
Enter fullscreen mode Exit fullscreen mode

In this case, you cannot use ^ + C or ^ + \ as they would terminate the session, rather than just the command. To terminate the command immediately, start a new line with #iex:break:

iex> ["abc"
...  "
...  ]
...  #iex:break
** (TokenMissingError) iex:1: incomplete expression
>
Enter fullscreen mode Exit fullscreen mode

3. Visual Debugging

In addition to the prying functionality provided by Elixir, there is also a more sophisticated Erlang debugger that you can use.

While it works with a single compiled file like IEx.break!, let's try using a file that is a part of a mix project with iex -S mix this time.

iex> :debugger.start()
{:ok, #PID<0.672.0>}
iex> :int.ni(Fib)
{:module, Fib}
iex> :int.break(Fib, 6)
ok
iex> Fib.number(5)
Enter fullscreen mode Exit fullscreen mode

The above will open the Erlang debugger and stop at the configured breakpoint.

This provides a more traditional debugging approach where you can perform a single step or continue to the next breakpoint, evaluate expressions in the current context, etc.

Erlang Debugger

If you are using Visual Studio Code, the ElixirLS plugin supports in-editor breakpoints.
There is also similar support in IntelliJ through the intellij-elixir plugin.

Debugging Elixir Processes

A post on debugging Elixir code wouldn't be complete without also covering how to debug processes.

While we can use the debugging methods we've already covered, a couple of other process-specific options are available.

Using Trace to Debug Processes in Elixir

Use :sys.trace/2 when you quickly want to see all the messages exchanged between a process and its state updates.
We can use it to start/stop logging the process states and messages. Let's continue with the Fibonacci computer, but this time, wrapped inside a GenServer:

iex(3)> {:ok, pid} = GenServer.start_link(Fib, nil)
{:ok, #PID<0.1023.0>}
iex(4)> :sys.trace(pid, true)
:ok
iex(5)> GenServer.call(pid, {:get, 1})
*DBG* <0.1023.0> got call {get,1} from <0.1004.0>
*DBG* <0.1023.0> sent 1 to <0.1004.0>, new state #{0 => 0,1 => 1}
1
iex(6)> GenServer.call(pid, {:get, 2})
*DBG* <0.1023.0> got call {get,2} from <0.1004.0>
*DBG* <0.1023.0> sent nil to <0.1004.0>, new state #{0 => 0,1 => 1}
nil
iex(3)> GenServer.cast(pid, {:compute, 10})
*DBG* <0.1031.0> got cast {compute,10}
:ok
*DBG* <0.1031.0> new state #{0 => 0,1 => 1,2 => 1,3 => 2,4 => 3,5 => 5,6 => 8,
                             7 => 13,8 => 21,9 => 34,10 => 55}
iex(4)> GenServer.call(pid, {:get, 2})
*DBG* <0.1031.0> got call {get,2} from <0.1029.0>
*DBG* <0.1031.0> sent 1 to <0.1029.0>, new state #{0 => 0,1 => 1,2 => 1,
                                                   3 => 2,4 => 3,5 => 5,
                                                   6 => 8,7 => 13,8 => 21,
                                                   9 => 34,10 => 55}
1
Enter fullscreen mode Exit fullscreen mode

Debugging Processes with Observer in Elixir

If you prefer a more visual approach, Erlang provides an :observer that opens a user interface you can use to browse the Supervision Tree or check process states and messages.

To access this, all you need is:

iex> :observer.start()
Enter fullscreen mode Exit fullscreen mode

While a full review of :observer would take up a whole new post, here is a small demo of all that is possible:

Wrap-up

In this post, we've covered three common methods of debugging: using the IO module, IEx, and visual debugging. We've also touched on debugging Elixir processes using trace and observer.

Elixir's powerful debugging tools are what make it such a compelling language choice for developers and businesses.

Until next time, enjoy getting stuck into debugging code and processes in Elixir!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Pulkit is a senior full-stack engineer and consultant. In his free time, he writes about his experiences on his blog.

💖 💪 🙅 🚩
pulkit110
Pulkit Goyal

Posted on December 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related