There are many ways to debug your Elixir code to fix errors. We always start with IO.inspect/2
and have used it for 90% of our debugging efforts. In this post, we’ll begin with some options to enhance IO.inspect/2
. Then we’ll to cover some additional debugging tools available to you in Elixir. If you set up error monitoring, this post will help you resolve those errors and get your Elixir applications back up and running as quickly as possible.
Elixir 1.14 added
Kernel.dbg/2
to improve the developer experience. Check out Debugging Elixir Phoenix with dbg to see howdbg
can replaceIO.inspect/2
in your debugging workflow.
Overview
We’re going to go over the techniques described in the Elixir Debugging docs to debug a simple Phoenix application.
- Create new Phoenix app
- IO.inspect/2
- IEx.pry/0 and IEx.break!/2
- Debugger
- Observer
- Phoenix LiveDashboard
Prerequisites
To follow this guide, make sure you have at least the following applications installed on your machine. In the brackets are the versions with which this guide was written.
- Elixir (1.12.3-otp-24)
- Erlang (OTP 24.1)
- Phoenix (1.6.2)
- Node.js with npm (15.14.0)
Create New Phoenix App
We are going to create a simple project called HelloDebugging. Use the command line to update your Phoenix version and create a new application.
$ mix archive.install hex phx_new 1.6.2
$ mix phx.new hello_debugging
Change into the project directory, create your database and start your server.
$ cd hello_debugging
$ mix ecto.create
$ mix phx.server
Now visit http://localhost:4000
and you should see the Welcome to Phoenix! page.
Add users
Now we want to add users to the project with the phx.gen html task. We’ll use the user create and update functions to see how the different debugging tools work.
mix phx.gen.html Accounts User users name:string email:string \
bio:string number_of_pets:integer
Follow the directions to add the resource to the router.ex
file and migrate the database with mix ecto.migrate
.
Now start your server with mix phx.server
and visit http://localhost:4000/users
on your local machine to see the index of users. Add a new user to test out the database.
We’ll use this project to learn about the different debugging tools.
IO.inspect/2
IO.inspect/2 is useful for debugging because it returns the item argument passed to it without affecting the behavior of the original code. Let’s imagine we are getting an unexpected error while updating our user. We’ll use IO.inspect/2
to dig into the issue.
Open the HelloDebugging.Accounts
module at lib/hello_debugging/accounts.ex
. We can add IO.inspect
throughout the Accounts.update_user/2
function.
def update_user(%User{} = user, attrs) do
user
|> IO.inspect()
|> User.changeset(attrs)
|> IO.inspect()
|> Repo.update()
|> IO.inspect()
end
Restart the server, visit http://localhost:4000/users
, and click Edit for the user you just created.
Now update a couple of the user attributes and click Save.
You should see the following in you Terminal.
%HelloDebugging.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: "A user",
email: "[email protected]",
id: 1,
inserted_at: ~N[2021-11-19 04:13:23],
name: "User",
number_of_pets: 1,
updated_at: ~N[2021-11-19 04:13:23]
}
#Ecto.Changeset<
action: nil,
changes: %{bio: "Updated bio", number_of_pets: 2},
errors: [],
data: #HelloDebugging.Accounts.User<>,
valid?: true
>
[debug] QUERY OK db=1.1ms queue=0.4ms idle=1177.4ms
UPDATE "users" SET "bio" = $1, "number_of_pets" = $2, "updated_at" = $3 WHERE "id" = $4 ["Updated bio", 2, ~N[2021-11-19 04:21:16], 1]
{:ok,
%HelloDebugging.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: "Updated bio",
email: "[email protected]",
id: 1,
inserted_at: ~N[2021-11-19 04:13:23],
name: "User",
number_of_pets: 2,
updated_at: ~N[2021-11-19 04:21:16]
}}
You can make out the original user, the changeset, and then the updated user. You’ll also see logging info from the database update.
As you can see IO.inspect/2
allows you 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.
label option
It isn’t too hard to scan through the output, but we can make it even easier to see the output associated with each function in the pipeline. also IO.inspect/2
provides the ability to decorate the output with a label
option.
def update_user(%User{} = user, attrs) do
user
|> IO.inspect(label: "original")
|> User.changeset(attrs)
|> IO.inspect(label: "changeset")
|> Repo.update()
|> IO.inspect(label: "updated")
end
Restart the server, visit http://localhost:4000/users
, and click Edit for the user you just created.
Now update a couple of the user attributes again and click Save. You should see the following output in your terminal.
original: %HelloDebugging.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: "Updated bio",
email: "[email protected]",
id: 1,
inserted_at: ~N[2021-11-19 04:13:23],
name: "User",
number_of_pets: 2,
updated_at: ~N[2021-11-19 04:21:16]
}
changeset: #Ecto.Changeset<
action: nil,
changes: %{bio: "Another update", number_of_pets: 5},
errors: [],
data: #HelloDebugging.Accounts.User<>,
valid?: true
>
[debug] QUERY OK db=0.7ms queue=0.4ms idle=1097.1ms
UPDATE "users" SET "bio" = $1, "number_of_pets" = $2, "updated_at" = $3 WHERE "id" = $4 ["Another update", 5, ~N[2021-11-19 04:28:05], 1]
updated: {:ok,
%HelloDebugging.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: "Another update",
email: "[email protected]",
id: 1,
inserted_at: ~N[2021-11-19 04:13:23],
name: "User",
number_of_pets: 5,
updated_at: ~N[2021-11-19 04:28:05]
}}
The original
, changeset
, and updated
data is labeled in the output.
binding()
You can also use IO.inspect/2
with binding()
, which returns all variable names and their values
def update_user(%User{} = user, attrs) do
IO.inspect binding()
user
|> User.changeset(attrs)
|> Repo.update()
end
Restart the server, visit http://localhost:4000/users
, and click Edit for the user you just created.
Now update a couple of the user attributes again and click Save. You should see the following output in your terminal.
[
attrs: %{
"bio" => "I added some pets",
"email" => "[email protected]",
"name" => "User",
"number_of_pets" => "8"
},
user: %HelloDebugging.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: "Another update",
email: "[email protected]",
id: 1,
inserted_at: ~N[2021-11-19 04:13:23],
name: "User",
number_of_pets: 5,
updated_at: ~N[2021-11-19 04:28:05]
}
]
We get a list of our variable names and their values.
Check out IO.inspect/2 to read more about other ways in which one could use this function. Also, to see the full list of formatting options to use with IO.inspect/2
, see Inspect.Opts.
While IO.inspect/2
will work for a majority of you debugging needs, next we’re going to cover some other more dynamic options.
IEx.pry/0 and IEx.break!/2
While IO.inspect/2
provides static output, Elixir’s interactive shell provides more dynamic ways to interact with debugged code.
The first one is with IEx.pry/0 which can be used instead of IO.inspect binding()
. Update the Accounts.update_user/2
function.
def update_user(%User{} = user, attrs) do
require IEx
IEx.pry()
user
|> User.changeset(attrs)
|> Repo.update()
end
Once the function above is executed inside an iex session, IEx will ask if we want to pry into the current code. If accepted, we will be able to access all variables, as well as imports and aliases from the code, directly From IEx. While pry is running, the code execution stops, until you call continue
. IEx will finish execution and it will start a new shell.
This time restart your server with an iex
session.
iex -S mix phx.server
Visit http://localhost:4000/users
, and click Edit for the user you created. Then update a couple of user attributes again and click Save.
Once you confirm you want to pry into the code, you can see the value of the attrs
variable. Then you enter continue
to finish execution.
Request to pry #PID<0.605.0> at HelloDebugging.Accounts.update_user/2 (lib/hello_debugging/accounts.ex:72)
70: def update_user(%User{} = user, attrs) do
71: require IEx
72: IEx.pry()
73:
74: user
Allow? [Yn] y
Interactive Elixir (1.12.3) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> attrs
%{
"bio" => "Two more!",
"email" => "[email protected]",
"name" => "User",
"number_of_pets" => "10"
}
pry(2)> continue
Unfortunately, similar to IO.inspect/2
, IEx.pry/0
also requires us to change the code we intend to debug.
Luckily IEx also provides a break!/2 function which allows you to set and manage breakpoints on any Elixir code without modifying its source.
Remove IEx.pry()
.
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
Restart your server with an iex
session again.
iex -S mix phx.server
Call break!/1
with the function we want to debug.
Visit http://localhost:4000/users
, and click Edit for the user you created. Then update a couple of user attributes again and click Save.
This time call whereami
while using pry to get debugging info before entering continue
to finish execution.
iex(2)> break! HelloDebugging.Accounts.update_user/2 1
Request to pry #PID<0.594.0> at HelloDebugging.Accounts.update_user/2 (lib/hello_debugging/accounts.ex:70)
68:
69: """
70: def update_user(%User{} = user, attrs) do
71: user
72: |> User.changeset(attrs)
Allow? [Yn] y
Interactive Elixir (1.12.3) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> whereami
Location: lib/hello_debugging/accounts.ex:70
68:
69: """
70: def update_user(%User{} = user, attrs) do
71: user
72: |> User.changeset(attrs)
(hello_debugging 0.1.0) HelloDebugging.Accounts.update_user/2
(hello_debugging 0.1.0) lib/hello_debugging_web/controllers/user_controller.ex:43: HelloDebuggingWeb.UserController.update/2
(hello_debugging 0.1.0) lib/hello_debugging_web/controllers/user_controller.ex:1: HelloDebuggingWeb.UserController.action/2
(hello_debugging 0.1.0) lib/hello_debugging_web/controllers/user_controller.ex:1: HelloDebuggingWeb.UserController.phoenix_controller_pipeline/2
(phoenix 1.6.2) lib/phoenix/router.ex:355: Phoenix.Router.__call__/2
(hello_debugging 0.1.0) lib/hello_debugging_web/endpoint.ex:1: HelloDebuggingWeb.Endpoint.plug_builder_call/2
(hello_debugging 0.1.0) lib/plug/debugger.ex:136: HelloDebuggingWeb.Endpoint."call (overridable 3)"/2
(hello_debugging 0.1.0) lib/hello_debugging_web/endpoint.ex:1: HelloDebuggingWeb.Endpoint.call/2
(phoenix 1.6.2) lib/phoenix/endpoint/cowboy2_handler.ex:43: Phoenix.Endpoint.Cowboy2Handler.init/4
pry(2)> continue
break!/1
is a great option of you don’t want to update your code to debug.
Debugger
If you are looking for a visual debugger with breakpoints, Erlang/OTP ships with a graphical debugger named :debugger
.
When you start the debugger, a Graphical User Interface (GUI) will open in your machine.
iex(2)> :debugger.start()
{:ok, #PID<0.569.0>}
iex(3)> :int.ni(HelloDebugging.Accounts)
{:module, HelloDebugging.Accounts}
iex(4)> :int.break(HelloDebugging.Accounts, 73)
:ok
We call :int.ni(HelloDebugging.Accounts)
to prepare our module for debugging. Then add a breakpoint
You can see we’ve set the breakpoint in the code.
Visit http://localhost:4000/users
, and click Edit for the user you created. Then update a couple of user attributes again and click Save.
We can see our process with break status in the debugger.
The process is blocked as in IEx.pry/0
. We can add a new breakpoint in the monitor window, inspect the code, see the variables and navigate it in steps.
Debugger has more options and command instructions that you can use. Check out the Debbuger docs for more information.
Observer
For debugging complex systems, sometime digging into 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. All of this info is available in Erlang with :observer
.
:observer.start
from an IEx sesion will open a GUI that provides many panes to fully understand and navigate the runtime and your Phoenix project.
Finally, you can also get a mini-overview of the runtime info by calling runtime_info/0
directly in IEx.
Phoenix LiveDashboard
LiveDashboard provides real-time performance monitoring and debugging tools for Phoenix developers. Often times it is easier to open Phoenix LiveDashboard than using remote observer in production.
Your Phoenix project has a link to LiveDashboard from the default homepage. You’ll see a number of different modules available in the tab bar.
One of the most useful tabs is the Processes list. You can sort by memory usage or reductions to debug performance issues.
Conclusion
If you’ve made it this far, we’re going to admit we’ve used IO.inspect/2
for 99% of our debugging efforts with the addition of :label
accounting for the other 1%. However, we learned a lot putting together this post, including how to debug using break!/2
without changing the code.
If you liked learning about debugging, we have an entire book about managing your app in production. See below for how to get the Quick Reference Guide from the Phoenix Deployment Handbook for free.