Skip to content

Instantly share code, notes, and snippets.

@ricardopadua
Last active September 27, 2023 15:24
Show Gist options
  • Save ricardopadua/b95724b312446377a5afc601e968e9bb to your computer and use it in GitHub Desktop.
Save ricardopadua/b95724b312446377a5afc601e968e9bb to your computer and use it in GitHub Desktop.
Debugging Elixir

Debugging - In this lesson we'll learn about debugging Elixir code as well as static analysis tools to help find potential bugs

IEx

The most straightforward tool we have for debugging Elixir code is IEx.

But don't be fooled by its simplicity - you can solve most of the issues with your application by it.

IEx means Elixir's interactive shell.

You could have already seen IEx in one of the previous lessons like Basics where we ran Elixir code interactively in the shell.

The idea here is simple.

You get the interactive shell in the context of the place you want to debug.

Let's try it.

To do that, create a file named test.exs and put this into the file:

defmodule TestMod do
  def sum([a, b]) do
    b = 0

    a + b
  end
end

IO.puts(TestMod.sum([34, 65]))

And if you run it - you'll get an apparent output of 34:

$ elixir test.exs
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test.exs:2

34

But now let's get to the exciting part - the debugging.

Put require IEx; IEx.pry in the line after b = 0 and let's try running it once again.

You'll get something like this:

$ elixir test.exs
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test.exs:2

Cannot pry #PID<0.92.0> at TestMod.sum/1 (test.exs:5). Is an IEx shell running?
34

You should note that vital message.

When running an application, as usual, IEx outputs this message instead of blocking execution of the program.

To run it properly you need to prepend your command with iex -S.

What this does is it runs mix inside the iex command so that it runs the application in a special mode, such that calls to IEx.pry stop the application execution.

For example, iex -S mix phx.server to debug your Phoenix application. In our case, it's going to be iex -r test.exs to require the file:

$ iex -r test.exs
Erlang/OTP 21 [erts-10.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test.exs:2

Request to pry #PID<0.107.0> at TestMod.sum/1 (test.exs:5)

    3:     b = 0
    4:
    5:     require IEx; IEx.pry
    6:
    7:     a + b

Allow? [Yn]

After responding to the prompt via y or pressing Enter, you've entered the interactive mode.

$ iex -r test.exs
Erlang/OTP 21 [erts-10.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test.exs:2

Request to pry #PID<0.107.0> at TestMod.sum/1 (test.exs:5)

    3:     b = 0
    4:
    5:     require IEx; IEx.pry
    6:
    7:     a + b

Allow? [Yn] y
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> a
34
pry(2)> b
0
pry(3)> a + b
34
pry(4)> continue
34

Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution

To quit IEx, you can either hit Ctrl+C two times to exit the app, or type continue to go to the next breakpoint.

As you can see, you can run any Elixir code.

However, the limitation is that you can't modify variables of existing code, due to language immutability.

However, you can get values of all the variables and run any computations.

In this case, the bug would be in b reassigned to 0, and sum function being buggy as a result.

Sure, language has already caught this bug even on the first run, but that's an example!

IEx.Helpers

One of the more annoying parts of working with IEx is it has no history of commands you used in previous runs.

For solving that problem, there is a separate subsection on IEx documentation, where you can find the solution for your platform of choice.

You can also look through the list of other available helpers in IEx.Helpers documentation.

Dialyxir and Dialyzer

The Dialyzer, a DIscrepancy AnaLYZer for ERlang programs is a tool for static code analysis. In other words they read but do not run code and analyse it e.g. looking for some bugs, dead, unnecessary or unreachable code.

The Dialyxir is a mix task to simplify usage of Dialyzer in Elixir.

Specifications help tools, like Dialyzer, better understand your code. Unlike documentation, which is human readable (if it exists and is well written), @spec uses a formal syntax better understood by tooling.

Let's add Dialyxir to our project. The simplest way is to add dependency to mix.exs file:

defp deps do
  [{:dialyxir, "~> 0.4", only: [:dev]}]
end

Then we call:

$ mix deps.get
...
$ mix deps.compile

The first command will download and install Dialyxir. You may be asked to install Hex along with it. The second compiles the Dialyxir application. If you want to install Dialyxir globally, please read its documentation.

The last step is to run Dialyzer to rebuild the PLT (Persistent Lookup Table). You need to do this every time after installation of a new version of Erlang or Elixir. Fortunately, Dialyzer will not try to analyze standard library every time that you try to use it. It takes a few minutes for the download to complete.

$ mix dialyzer --plt
Starting PLT Core Build ...
this will take awhile
dialyzer --build_plt --output_plt /.dialyxir_core_18_1.3.2.plt --apps erts kernel stdlib crypto public_key -r /Elixir/lib/elixir/../eex/ebin /Elixir/lib/elixir/../elixir/ebin /Elixir/lib/elixir/../ex_unit/ebin /Elixir/lib/elixir/../iex/ebin /Elixir/lib/elixir/../logger/ebin /Elixir/lib/elixir/../mix/ebin
  Creating PLT /.dialyxir_core_18_1.3.2.plt ...
...
 done in 5m14.67s
done (warnings were emitted)

Static analysis of code

Now we're ready to use Dialyxir:

$ mix dialyzer
...
examples.ex:3: Invalid type specification for function 'Elixir.Examples':sum_times/1.
The success typing is (_) -> number()
...

The message from Dialyzer is clear: the return type of our function sum_times/1 is different than declared. This is because Enum.sum/1 returns a number and not a integer but the return type of sum_times/1 is integer.

Since number is not integer we get an error. How do we fix it? We need to use the round/1 function to change our number to an integer:

@spec sum_times(integer) :: integer
def sum_times(a) do
  [1, 2, 3]
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Finally:

$ mix dialyzer
...
  Proceeding with analysis...
done in 0m0.95s
done (passed successfully)

Using specifications with tools to perform static code analysis helps us make code that is self-tested and contains less bugs.

Debugging

Sometimes static analysis of code is not enough. It may be necessary to understand the execution flow in order to find bugs. The simplest way is to put output statements in our code like IO.puts/2 to track values and code flow, but this technique is primitive and has limitations. Thankfully for us, we can use the Erlang debugger to debug our Elixir code.

Let’s look at a basic module:

defmodule Example do
  def cpu_burns(a, b, c) do
    x = a * 2
    y = b * 3
    z = c * 5

    x + y + z
  end
end

Then run iex:

iex -S mix

And run debugger:

iex > :debugger.start()
{:ok, #PID<0.307.0>}

The Erlang :debugger module provides access to the debugger. We can use the start/1 function to configure it:

  • An external configuration file can be used by passing the file path.
  • If the argument is :local or :global then debugger will:
    • :global – debugger will interpret code on all known nodes. This is default value.
    • :local – debugger will interpret code only on current node.

The next step is to attach our module to debugger:

iex > :int.ni(Example)
{:module, Example}

The :int module is an interpreter that gives us the ability to create breakpoints and step through the execution of the code.

When you start the debugger you will see a new window like this:

Debugger Screenshot 1

After we've attached our module to the debugger it will be available in the menu on the left:

Debugger Screenshot 2

Creating breakpoints

A breakpoint is a point in the code where execution will be halted. We have two ways of creating breakpoints:

  • :int.break/2 in our code
  • The debugger's UI

Let's try to create a breakpoint in IEx:

iex > :int.break(Example, 8)
:ok

This sets a breakpoint on line 8 of the Example module. Now when we call our function:

iex > Example.cpu_burns(1, 1, 1)

Execution will be paused in IEx and the debugger window should look like this:

Debugger Screenshot 3

An additional window with our source code will appear:

Debugger Screenshot 4

In this window we can look up the value of variables, step forward to next line, or evaluate expressions. :int.disable_break/2 can be called in order to disable a breakpoint:

iex > :int.disable_break(Example, 8)
:ok

To re-enable a breakpoint we can call :int.enable_break/2 or we can remove a breakpoint like this:

iex > :int.delete_break(Example, 8)
:ok

The same operations are available in the debugger window. In the top menu, Break, we can select Line Break and setup breakpoints. If we select a line that does not contain code then breakpoints will be ignored, but it will appear in the debugger window. There are three types of breakpoints:

  • Line breakpoint — debugger suspends execution when we reach the line, setup with :int.break/2
  • Conditional breakpoint — similar to the line breakpoint but the debugger suspends only when the specified condition is reached, these are setup using :int.get_binding/2
  • Function breakpoint — debugger will suspend on first line of a function, configured using :int.break_in/3

That's all! Happy debugging!

Debugging - The Elixir programming language {"@context":"https://schema.org","@type":"WebPage","description":"Website for Elixir","headline":"Debugging","url":"https://elixir-lang.org/getting-started/debugging.html"}

Debugging

There are a number of ways to debug code in Elixir. In this chapter we will cover some of the more common ways of doing so.

IO.inspect/2

What makes IO.inspect(item, opts \\ []) really useful in debugging is that it returns the item argument passed to it without affecting the behavior of the original code. Let’s see an example.

(1..10)
|> IO.inspect
|> Enum.map(fn x -> x * 2 end)
|> IO.inspect
|> Enum.sum
|> IO.inspect

Prints:

1..10
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
110

As you can see IO.inspect/2 makes it possible to “spy” on values almost anywhere in your code without altering the result, making it very helpful inside of a pipeline like in the above case.

IO.inspect/2 also provides the ability to decorate the output with a label option. The label will be printed before the inspected item:

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

Prints:

before: [1, 2, 3]
after: [2, 4, 6]

It is also very common to use IO.inspect/2 with binding(), which returns all variable names and their values:

def some_fun(a, b, c) do
  IO.inspect binding()
  ...
end

When some_fun/3 is invoked with :foo, "bar", :baz it prints:

[a: :foo, b: "bar", c: :baz]

Please see IO.inspect/2 to read more about other ways in which one could use this function. Also, in order to find a full list of other formatting options that one can use alongside IO.inspect/2, see Inspect.Opts.

dbg

Elixir v1.14 introduced dbg/2. dbg is similar to IO.inspect/2, but specifically tailored for debugging. It prints the value passed to it and returns it (just like IO.inspect/2), but it also prints the code and location.

# In my_file.exs
feature = %{name: :dbg, inspiration: "Rust"}
dbg(feature)
dbg(Map.put(feature, :in_version, "1.14.0"))

The code above prints this:

[my_file.exs:2: (file)]
feature #=> %{inspiration: "Rust", name: :dbg}
[my_file.exs:3: (file)]
Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg}

When talking about IO.inspect/2, we mentioned its usefulness when placed between steps of |> pipelines. dbg does it better: it understands Elixir code, so it will print values at every step of the pipeline.

# In dbg_pipes.exs
__ENV__.file
|> String.split("/", trim: true)
|> List.last()
|> File.exists?()
|> dbg()

This code prints:

[dbg_pipes.exs:5: (file)]
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
|> List.last() #=> "dbg_pipes.exs"
|> File.exists?() #=> true

Note dbg only supports stepping for pipelines (in other words, it can only step through the code it sees). For general stepping of functions, you will need to set breakpoints using IEx.break!/4.

Breakpoints

When code calling dbg is executed via iex, IEx will ask you to “stop” the code execution where the dbg call is. If you accept, you’ll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called “prying”. While the pry session is running, the code execution stops, until continue or next are called. Remember you can always run iex in the context of a project with iex -S mix TASK.

dbg calls require us to change the code we intend to debug and has limited stepping functionality. Luckily IEx also provides a break!/2 function which allows you to set and manage breakpoints on any Elixir code without modifying its source:

See the example in asciinema

Similar to dbg, once a breakpoint is reached code execution stops until continue or next are invoked. However, break!/2 does not have access to aliases and imports from the debugged code as it works on the compiled artifact rather than on source code.

Observer

For debugging complex systems, jumping at the code is not enough. It is necessary to have an understanding of the whole virtual machine, processes, applications, as well as set up tracing mechanisms. Luckily this can be achieved in Erlang with :observer. In your application:

$ iex
iex> :observer.start()

When running iex inside a project with iex -S mix, observer won’t be available as a dependency. To do so, you will need to call the following functions before:

iex> Mix.ensure_application!(:wx)
iex> Mix.ensure_application!(:runtime_tools)
iex> Mix.ensure_application!(:observer)
iex> :observer.start()

If any of the calls above fail, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named erlang vs erlang-nox on Debian/Ubuntu/Arch). In others managers, you may need to install a separate erlang-wx (or similarly named) package.

There are conversations to improve this experience in future releases.

The above will open another Graphical User Interface that provides many panes to fully understand and navigate the runtime and your project:

Observer GUI screenshot

We explore the Observer in the context of an actual project in the Dynamic Supervisor chapter of the Mix & OTP guide. This is one of the debugging techniques the Phoenix framework used to achieve 2 million connections on a single machine.

If you are using the Phoenix web framework, it ships with the Phoenix LiveDashboard, a web dashboard for production nodes which provides similar features to Observer.

Finally, remember you can also get a mini-overview of the runtime info by calling runtime_info/0 directly in IEx.

Other tools and community

We have just scratched the surface of what the Erlang VM has to offer, for example:

  • Alongside the observer application, Erlang also includes a :crashdump_viewer to view crash dumps

  • Integration with OS level tracers, such as Linux Trace Toolkit, DTRACE, and SystemTap

  • Microstate accounting measures how much time the runtime spends in several low-level tasks in a short time interval

  • Mix ships with many tasks under the profile namespace, such as cprof and fprof

  • For more advanced use cases, we recommend the excellent Erlang in Anger, which is available as a free ebook

Happy debugging!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment