As we continue exploring IEx capabilities, let's focus on its powerful debugging features. This article will show you practical techniques for debugging Elixir applications using IEx.
Table of Contents
- Interactive Debugging with IEx.pry
- Debugging Real Applications
- Advanced Debugging Techniques
- Best Practices
- Conclusion
- Next Steps
Interactive Debugging with IEx.pry
The IEx.pry/0
function is one of the most powerful debugging tools in Elixir. Let's see it in action with a practical example:
# user_points.ex
defmodule UserPoints do
require IEx # Required to use IEx.pry
def calculate_bonus(points) do
bonus =
points
|> multiply_points()
|> apply_threshold()
|> (fn points ->
IEx.pry() # Add breakpoint here
round_points(points)
end).()
{:ok, bonus}
end
defp multiply_points(points) do
points * 1.5
end
defp apply_threshold(points) when points > 1000 do
1000
end
defp apply_threshold(points), do: points
defp round_points(points) do
round(points)
end
end
To use this in practice:
- Save the code in
user_points.ex
- Start IEx with the code:
iex user_points.ex
- Try the function:
iex(1)> UserPoints.calculate_bonus(800)
Break reached: UserPoints.calculate_bonus/1 (user_points.ex:10)
7: |> multiply_points()
8: |> apply_threshold()
9: |> (fn points ->
10: IEx.pry() # Add breakpoint here
11: round_points(points)
12: end).()
13:
# Let's analyze the flow to this point:
iex(2)> points # Current value after multiply_points and apply_threshold
1000
# We can also check intermediate calculations:
iex(3)> 800 * 1.5 # What multiply_points did
1200.0
# Since 1200.0 > 1000, apply_threshold returned 1000
iex(4)> continue # Let's continue execution
{:ok, 1000} # Final result
At the breakpoint, we can:
- Inspect variables (like
points
) - Execute code in the current context
- Use
continue
to resume execution
Debugging Real Applications
Let's look at a more complex example - a shopping cart system with multiple discount rules:
# shopping_cart.ex
defmodule ShoppingCart do
defstruct items: [], discounts: [], total: 0
def new, do: %ShoppingCart{}
def add_item(cart, item) do
require IEx; IEx.pry() # Debug point 1
%{cart | items: [item | cart.items]}
|> calculate_total()
|> apply_discounts()
end
def calculate_total(%{items: items} = cart) do
total = Enum.reduce(items, 0, fn
%{price: price, quantity: quantity}, acc ->
acc + (price * quantity)
end)
%{cart | total: total}
end
def apply_discounts(%{total: total, items: items} = cart) do
require IEx; IEx.pry() # Debug point 2
discounts = [
quantity_discount(items),
total_discount(total)
]
%{cart |
discounts: discounts,
total: apply_discount_values(total, discounts)
}
end
defp quantity_discount(items) do
total_quantity = Enum.reduce(items, 0, & &1.quantity + &2)
case total_quantity do
q when q >= 5 -> {:quantity, 0.1}
_ -> {:quantity, 0}
end
end
defp total_discount(total) do
case total do
t when t >= 100 -> {:total, 0.15}
_ -> {:total, 0}
end
end
defp apply_discount_values(total, discounts) do
Enum.reduce(discounts, total, fn
{_type, value}, acc -> acc * (1 - value)
end)
end
end
Let's debug this cart system:
# Start IEx with the module
iex(1)> c("shopping_cart.ex")
warning: redefining module ShoppingCart (current version defined in memory)
[ShoppingCart]
iex(2)> cart = ShoppingCart.new()
%ShoppingCart{items: [], discounts: [], total: 0}
iex(3)> item = %{name: "Phone", price: 500, quantity: 2}
%{name: "Phone", price: 500, quantity: 2}
iex(4)> ShoppingCart.add_item(cart, item)
Break reached: ShoppingCart.add_item/2 (shopping_cart.ex:7)
4: def new, do: %ShoppingCart{}
5:
6: def add_item(cart, item) do
7: require IEx; IEx.pry() # Debug point 1
8: %{cart | items: [item | cart.items]}
9: |> calculate_total()
10: |> apply_discounts()
# At Debug point 1:
iex(5)> cart
%ShoppingCart{items: [], discounts: [], total: 0}
iex(6)> item
%{name: "Phone", price: 500, quantity: 2}
iex(7)> continue
Break reached: ShoppingCart.apply_discounts/1 (shopping_cart.ex:22)
19: end
20:
21: def apply_discounts(%{total: total, items: items} = cart) do
22: require IEx; IEx.pry() # Debug point 2
23: discounts = [
24: quantity_discount(items),
25: total_discount(total)
# At Debug point 2:
iex(8)> cart.total
1000
iex(9)> continue
%ShoppingCart{
items: [%{name: "Phone", price: 500, quantity: 2}],
discounts: [quantity: 0, total: 0.15],
total: 850.0
}
Advanced Debugging Techniques
1. Using IO.inspect/2
IO.inspect/2
is a non-blocking way to debug your code by inspecting values as they flow through your functions:
def process_order(items) do
items
|> IO.inspect(label: "Input items")
|> calculate_total()
|> IO.inspect(label: "After total calculation")
|> apply_discounts()
|> IO.inspect(label: "After discounts")
end
# Example output:
# Input items: [%{name: "Phone", price: 500, quantity: 2}]
# After total calculation: 1000
# After discounts: 850.0
2. Conditional Debugging
You can use environment variables to control when debugging code runs:
defmodule UserPoints do
require IEx
def calculate_bonus(points) do
bonus =
points
|> multiply_points()
|> debug_threshold()
|> round_points()
{:ok, bonus}
end
defp debug_threshold(points) do
if System.get_env("DEBUG") do
IEx.pry()
end
apply_threshold(points)
end
# ... rest of the module
end
To use conditional debugging:
# Normal execution
$ iex user_points.ex
iex(1)> UserPoints.calculate_bonus(800)
{:ok, 1000}
# With debugging enabled
$ DEBUG=true iex user_points.ex
iex(1)> UserPoints.calculate_bonus(800)
# Will break at the debug point
Best Practices
-
Strategic Breakpoints
- Place
IEx.pry()
at critical decision points - Remove debugging code before committing
- Use meaningful variable names at debug points
- Place
-
Effective Debugging Session
- Start with high-level inspection
- Check intermediate values
- Use IO.inspect for passive debugging
- Keep track of the execution flow
-
Code Organization
- Keep debugging code clearly marked
- Use version control branches for debugging sessions
- Document discovered issues
- Clean up debugging code after use
-
Development Workflow
- Use
IEx.pry()
for interactive debugging - Use
IO.inspect/2
for passive debugging in pipelines - Add temporary debugging code in development
- Use comprehensive tests to prevent bugs
- Use
Conclusion
IEx.pry() is a powerful tool for interactive debugging in Elixir. Combined with other techniques like IO.inspect/2 and strategic breakpoints, it provides a robust debugging experience.
Remember that these are development tools - they should be used carefully in production environments.
Next Steps
- Practice debugging with your own projects
- Learn about debugging strategies for different types of problems
Top comments (0)