Debugging Elixir

Sun, Jul 11, 2021 5 min read
Different techniques to help you debug code in Elixir.

Debugging is being the detective in crime movies where you are also the murderer.

Detectives have tools to help them. They can be old-fashioned like Sherlock Holmes and need a magnifying glass or go full CSI and have a computer that cross-checks fingerprints slowly showing one at a time on the tv screen. 🀦

You will indeed commit crimes πŸ’©πŸ› during development, so what tools does Elixir have to help you?

Old school IO.puts

There are many better tools for that, but the good old print will save your day many times.

iex(3)> username = "ΒΊ-ΒΊ"
"ΒΊ-ΒΊ"
iex(4)> IO.puts("------>" <> username)
------>ΒΊ-ΒΊ
:ok

The problem with it is that we have to print a string or string-compatible format.

Old school with steroids IO.inspect

Doing IO.puts is not ideal, especially if you want to print info about some complex data structure like a map or a tuple.

IO.inspect got your back. It will do the proper conversion to a string and display helpful information.

iex(1)> conn = %{status: 504}
%{status: 504}
iex(2)> IO.inspect(conn)
%{status: 504}
%{status: 504}

But how to debug what happens during a pipeline?

["some", "weird", "debugging"]
|> does_some_stuff()
|> does_more_stuff()
|> does_even_more_stuff()

Immutability is incredible; it allows you to know that an error is happening in one of the above functions. But which one?

You could do the following:

stuff =
  ["some", "weird", "debugging"]
  |> does_some_stuff()

IO.inspect(stuff)

stuff
|> does_more_stuff()
|> does_even_more_stuff()

But IO.inspect is a perfect match for this because it returns its argument unchanged so you can just do:

["some", "weird", "debugging"]
|> does_some_stuff()
|> IO.inspect()
|> does_more_stuff()
|> does_even_more_stuff()

Awesome right? 😎

You can even add labels to help you during this kind of debugging:

["some", "weird", "debugging"]
|> does_some_stuff()
|> IO.inspect(label: "suspect num 1")
|> does_more_stuff()
|> IO.inspect(label: "suspect num 2")
|> does_even_more_stuff()

IEX.pry magnifying glass

Sometimes, instead of printing every variable, you want to check some variables or see the results of applying a function given a specific state.

IEX.pry will help you with that. It will allow you to stop the application in a specific line in your code.

Consider the following example. There’s a crime being committed, but we don’t know where. πŸ™ˆπŸ™‰πŸ™Š

defmodule Crime do
  def double_or_nothing(number) do
    number / 0 + 10
  end
end

To use IEX.pry you just need to add the following to the place you want to debug:

require IEX; IEX.pry

So in this case is just doing the following:

defmodule Crime do
  def double_or_nothing(number) do
    require IEx; IEx.pry
    number / 0 + 10
  end
end

Then you can start a mix session:

$ iex -S mix

iex(1)> Crime.double_or_nothing(10)
Break reached: Crime.double_or_nothing/1 (iex:2)
pry(1)> i number
Term
  10
Data type
  Integer
Reference modules
  Integer
Implemented protocols
  IEx.Info, Inspect, List.Chars, String.Chars
pry(2)> number
10
pry(3)> number + 10
20
pry(4)> h # if you need help
...
pry(5)> respawn()

You finish by calling respawn to continue execution in a new shell session.

This approach is great but has some cons 😐:

If you want to have multiple debug steps, add require IEx; IEx.pry for each, and continue to the next breakpoint.

IEx break! it out

Instead of needing to hardcode the IEX.pry breakpoints, you can just set a breakpoint before executing the code.

Using the same example:

defmodule Crime do
  def double_or_nothing(number) do
    number / 0 + 10
  end
end

You can then:

$ iex -S mix

iex(1)> break! Crime.double_or_nothing/1
1
iex(2)> Crime.double_or_nothing(20)
Break reached: Crime.double_or_nothing/1 (lib/crime.ex:2)

    1: defmodule Crime do
    2:   def double_or_nothing(number) do
    3:     number / 0 + 10
    4:   end
pry(1)> i number
Term
  20
Data type
  Integer
Reference modules
  Integer
Implemented protocols
  IEx.Info, Inspect, List.Chars, String.Chars
pry(2)> whereami
Location: lib/crime.ex:2

    1: defmodule Crime do
    2:   def double_or_nothing(number) do
    3:     number / 0 + 10
    4:   end

    (crime 0.1.0) Crime.double_or_nothing/1
pry(3)> respawn()

We didn’t need to change any line of our code to set the breakpoint, which is a lot better. πŸ‘Œ You can also specify in break the number of times it will break.

Testing with IEx.pry

If you want to debug a test if you add require IEx; IEx.pry to it. When you run the tests with mix test, you will get the following error:

Cannot pry #PID<0.123.0> at Crime.CrimeTest ...
Is an IEx shell running?

This is because you are not inside an interactive shell session. Simple enough, let’s run tests in an interactive shell. For that run:

iex -S mix test

Avoiding timeouts ⏰

To avoid timeouts, run tests with the trace option:

$ iex -S mix test --trace

VSCode and ElixirLS

Left the best for last.

If you are a Visual Studio Code user, you are in a good place for debugging Elixir.

The first step is to install the ElixirLS extension.

Then you need to add a configuration file to set up debug. Finally, click on “Run and Debug”.

After that, click on “create a launch.json file”. This will create a default configuration for Elixir.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "mix_task",
      "name": "mix (Default task)",
      "request": "launch",
      "projectDir": "${workspaceRoot}"
    },
    {
      "type": "mix_task",
      "name": "mix test",
      "request": "launch",
      "task": "test",
      "taskArgs": ["--trace"],
      "startApps": true,
      "projectDir": "${workspaceRoot}",
      "requireFiles": ["test/**/test_helper.exs", "test/**/*_test.exs"]
    }
  ]
}

I like to add a debug mix task to run some code I want to debug. For that, add the following to the lauch.json file.

    {
      "type": "mix_task",
      "name": "debug",
      "request": "launch",
      "startApps": true,
      "projectDir": "${workspaceRoot}",
      "task": "debug",
    },

And inside your project, add the task file:

lib/mix/tasks.debug

defmodule Mix.Tasks.Debug do
  use Mix.Task

  @impl Mix.Task
  def run(_args) do
    IO.puts("debugging...")

    # Code to test
    Crime.double_or_nothing(10)
  end
end

With this configuration in place, you are ready, set, go for debugging. You can either debug when running a test or execute the code you want to debug in the debug mix task.

Add a breakpoint(s) to the code you want to check.

And then click to start the debug session.

It will stop at the breakpoint to inspect values, add watches and step into and over the following lines.

With these tools, you are ready to solve any crime you commit in your code.

References