DEV Community

Cover image for Debugging in Elixir and Erlang: An Introduction
Marcos Ramos for AppSignal

Posted on • Originally published at blog.appsignal.com

Debugging in Elixir and Erlang: An Introduction

Welcome to part one of this two-part series on debugging in Elixir and Erlang. In this post, we'll use several different tools and techniques to debug Elixir code.

First, we'll get to know the demo project I created to showcase certain tools: dist_messenger. It's an Elixir project of a distributed messaging system that can be used directly on IEx. It allows users in different nodes to send and receive messages to each other.

Then, we'll learn about the basic debugging tools of Elixir: inspect, pry and dbg.

In part two, we'll turn our attention to Erlang, putting together what we learn into a simple script to trace a process message across nodes.

Let's get going!

The Necessity of Debugging

You write a piece of code, you run it, and it doesn't execute as expected.

That is what programming looks like most of the time. Trying to figure out what's happening can not only be time-consuming, but very frustrating when we don't have the right tools.

Can you level your washing machine without a leveling tool, instead using a ruler? Of course, but it will take much more time and energy.

So, to understand what's happening in our code, it's good sometimes to take a few steps back and ask some questions, like:

  • Is the value of this variable correct?
  • Are the transformations being applied to a variable producing the expected result?
  • Are the messages being correctly sent from one process to another?
  • Is there any abnormality in resource consumption?

Both Elixir and Erlang provide tools to answer these questions. Knowing how to use the tools certainly adds a lot of value.

Getting to Know Our Demo Project

The dist_messenger project uses a combination of GenServer and :global to provide a distributed messaging system inside an Erlang cluster.

Each node in this cluster can handle one user, and its state is maintained by a single GenServer, the Messenger.MessagingServer. Users can sign in to the cluster with a unique name, which is then registered across all nodes using the :global.register_name/2 function.

For the user interface, the Messenger.IExClient provides several functions that allow users to interact with the messaging system in IEx. This module is imported by the .iex.exsscript, so we can use all its functions directly on iex when running it with iex -S mix. Check the docs for .iex.exs
files
.

Because each node handles one user, to get the full experience of this project, we need to run multiple nodes (by running nodes with a node name and a shared secret cookie). To make things simple, the project includes a script start that will do that for us. You just need to give the name (it will run all nodes on 0.0.0.0).

You'll notice that the included .iex.exs script will automatically try to connect to nodes node0 up to node9 on localhost. So, by naming the nodes within this range, we should get a cluster.

Let's start two nodes and sign-in two users: user alice on node0 and bob on node1.

# On terminal 1
$ ./start node0

# On terminal 2
$ ./start node0
Enter fullscreen mode Exit fullscreen mode

To sign in to the messaging system, you can call sign in <name> on iex:

# node0 - user: alice
iex> sign in "alice"
Signed in!
Enter fullscreen mode Exit fullscreen mode

Now you can send a message to other users using msg <user> <message>:

# node0 - user: alice
iex> msg "bob", "Hello! How are you?"
Message sent!
Enter fullscreen mode Exit fullscreen mode

In bob's session, we get a notification that a new message has arrived:

# node1 - user: bob
[!] New message from alice
Enter fullscreen mode Exit fullscreen mode

We can use the inbox command to view all received messages:

# node1 - user: bob
iex> inbox
> from: alice
> Hello! How are you?
---
Enter fullscreen mode Exit fullscreen mode

In alice's session, we can use sent to view all sent messages:

# node0 - user: alice
iex> sent
> to: bob
> Hello! How are you?
---
Enter fullscreen mode Exit fullscreen mode

There are also other commands:

  • signout: signs out the current users from the cluster
  • whoami: prints the current signed-in user
  • last_msg: prints the last received message
  • editor: sends a message like msg, but it's interactive

Now that we know how our example works, let's get started with debugging!

Elixir Debugging Basics

You can use the tools in this section to:

  • quickly gather information about a specific variable
  • get the result of a function call
  • check a whole pipe chain

They do require changes in the source code, so we'll use them during development and tests.

Debugging with IO.inspect/2 in Elixir

This is the easiest — and probably the first — tool that an Elixir developer will reach for to investigate issues. The IO.inspect/2 function prints the contents of an expression and returns the expression itself.

iex> name = {:name, "Jack Shephard"}
iex> IO.inspect(name, label: "Name")
# Name: {:name, "Jack Shephard"}
Enter fullscreen mode Exit fullscreen mode

It returns the value of the evaluated expression so that you can use it within a pipeline. The label option is very useful when you have multiple calls to IO.inspect and need some hints to understand what you are looking at.

person = %{name: "Kate Austen"}

iex> " some text "
...> |> String.trim()
...> |> String.upcase() |> IO.inspect(label: "Here")
...> |> String.split()
Here: "SOME TEXT"
["SOME", "TEXT"]
Enter fullscreen mode Exit fullscreen mode

IEx.pry for Elixir

While IO.inspect allows you to read the content of any variable, IEx.pry gives us a bit more insight and information on the code.

You can think of IO.pry as an interactive IO.inspect.

Let's use it in the editor/0 function and see what happens:

...

def editor do
  case Messaging.whoami() do
    {:ok, _user} ->
      recipient = IO.gets("recipient: ") |> String.trim()
      message = IO.gets("message: ") |> String.trim()

      # Pause the execution here
      require IEx; IEx.pry

      msg(recipient, message)

    _ ->
      IO.puts("Not signed in!")
  end
end

...
Enter fullscreen mode Exit fullscreen mode

Note: you must use IEx in the application context in order to use IEx.pry. This means you need to start it with iex -S mix.

When you call the editor/0 (after you type in the recipient and the message), the iex session will pause execution on the line.

iex> editor
recipient: kate.austen
message: Hello, Kate!
Break reached: Messenger.IExClient.editor/0 (lib/messenger/iex_client.ex:96)

   93:         recipient = IO.gets("recipient: ") |> String.trim()
   94:         message = IO.gets("message: ") |> String.trim()
   95:
   96:         require IEx; IEx.pry
   97:
   98:         msg(recipient, message)
   99:

pry>
Enter fullscreen mode Exit fullscreen mode

In this new console, you can type a variable's name to see its contents:

pry> recipient
"kate.austen"
pry> message
"Hello, Kate!"
Enter fullscreen mode Exit fullscreen mode

Type continue or next to continue the execution.

dbg/2 Debugging in Elixir

This is a new feature since Elixir 1.14. It's pretty much IO.inspect/2 with superpowers.

To demonstrate it, we'll add dbg to this demo line. It will be called whenever the server receives a message from another server.

# ...

def handle_info({:msg, from, body}, %{inbox: inbox} = state) do
  # ...

  message = {from, body}
  updated_state = %{state | inbox: [message | inbox]} |> dbg()

  {:noreply, updated_state}
end

#...
Enter fullscreen mode Exit fullscreen mode

Now, when your node receives a message, the recipient's iex session will request to pry on the code:

[!] New message from jacob
Request to pry #PID<0.159.0> at Messenger.MessagingServer.handle_info/2 (lib/messenger/messaging_server.ex:82)

   79:     end
   80:
   81:     message = {from, body}
   82:     updated_state = %{state | inbox: [message | inbox]} |> dbg()
   83:
   84:     {:noreply, updated_state}
   85:   end

Allow? [Yn]
Enter fullscreen mode Exit fullscreen mode

This will open an IEx.pry session, and you can check the values of all variables.

You can also use dbg at the end of a pipe to see what's happening:

[["h"], ["i"]] #=> [["h"], ["i"]
|> List.flatten() #=> ["h", "i"]
|> Enum.join() #=> "hi
|> dbg()
Enter fullscreen mode Exit fullscreen mode

dbg/2 is macro-evaluated at compile time. When called, it will inject code
that allows the interpreter to pause an application's execution and pry the
current context. Check out 'Elixir 1.14: Better Debugging with dbg/2 and More' for an overview of dbg/2.

Debugging Production Apps with AppSignal

After deploying your app to production, AppSignal can help debug issues from the real world.

Set up AppSignal for your Elixir application, and you'll automatically gain access to an error dashboard with information about all the errors in your app.

Errors dashboard

From there, you can dig deeper for more information on individual errors, so you can fix them:

Specific errors in Elixir

Read more about AppSignal for Elixir.

Wrap Up

This post has introduced debugging basics in Elixir using IO.inspect/2, IEx.pry, and dbg/2. Check out Three Ways to Debug Code in Elixir for some more information.

Next time, we'll focus on Erlang specifically, debugging runtime and finding out how to trace an Elixir clustered application using Erlang's :debugger.

Until then, happy coding!

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!

Top comments (0)