When writing Elixir, most developers quickly get familiar with IO.inspect
as a quick way to see what’s happening inside their code. But what many overlook is that IO.inspect
is far more powerful than just a method that prints a variable to the console.
In fact, with the right options and placement, IO.inspect
can become a precise, highly targeted debugging tool, one that doesn’t interrupt your program flow and works seamlessly with Elixir’s functional pipelines.
This post will walk you through both the fundamentals and advanced patterns for using IO.inspect
effectively. By the end, you’ll know how to control output formatting, label your prints for clarity, debug concurrent processes, and even integrate conditional or file-based inspection.
The Basics of IO.inspect
for…
When writing Elixir, most developers quickly get familiar with IO.inspect
as a quick way to see what’s happening inside their code. But what many overlook is that IO.inspect
is far more powerful than just a method that prints a variable to the console.
In fact, with the right options and placement, IO.inspect
can become a precise, highly targeted debugging tool, one that doesn’t interrupt your program flow and works seamlessly with Elixir’s functional pipelines.
This post will walk you through both the fundamentals and advanced patterns for using IO.inspect
effectively. By the end, you’ll know how to control output formatting, label your prints for clarity, debug concurrent processes, and even integrate conditional or file-based inspection.
The Basics of IO.inspect
for Elixir
IO.inspect is defined like this:
item
: Any Elixir value you want to inspect.opts
: Keyword list of options that controls how the term is printed, as defined in Inspect.Opts. By default, it writes the inspected value to the standard output (:stdio
) and then returns the term unchanged.
That last part is key. Because it returns the term, you can drop it anywhere in a function or pipeline without breaking the flow.
Example:
Prints:
Caveat: When IO.inspect
Is the Last Call in a Function
Because IO.inspect
returns its argument, if it’s the last expression in your function, that inspected value becomes the function’s return value.
That’s fine if you intend to return it, but it can lead to subtle bugs if you were only printing it for debugging and expected a different return.
Take this example:
Here, the function returns the user struct as normal, which is fine.
Now, consider:
This returns "Done!"
instead of, say, :ok
.
If you want to print something but return a different value, make the return explicit:
Or if you’re in a pipeline:
Rule of thumb: if IO.inspect
is the last expression in a function, be explicit about what you want to return.
Strategic Placement with the Pipe Operator
Because IO.inspect
returns its argument, it fits perfectly in the middle of a pipeline:
This lets you peek into the data flow at exactly the point you want, without rewriting your code into intermediate variables.
A neat trick is to place multiple IO.inspect
calls at different stages, each with a distinct label, so you can see how the data changes step by step.
Using Labels for Clarity in Elixir
Without labels, multiple inspection outputs can be hard to tell apart. The label
option solves this:
Prints:
When debugging a pipeline with several inspect points, labels make the output self-describing. This is especially useful when you’re debugging multiple similar data structures in the same run.
Pretty Printing and Formatting Output
Sometimes, especially with large maps or deeply nested lists, the default single-line output is hard to read. That’s where formatting options defined in Inspect.Opts come in.
Option | Description | Default |
---|---|---|
:pretty | If set to true , enables pretty printing. | false |
:limit | Limits the number of items inspected for tuples, bitstrings, maps, lists and any other collection of items, with the exception of printable strings and printable charlists which use the :printable_limit option. If you don’t want to limit the number of items to a particular number, use :infinity . It accepts a positive integer or :infinity . | 50 |
:printable_limit | Limits the number of characters that are inspected on printable strings and printable charlists. You can use String.printable?/1 and List.ascii_printable?/1 to check if a given string or charlist is printable. If you don’t want to limit the number of characters to a particular number, use :infinity . It accepts a positive integer or :infinity . | 4096 |
:width | Number of characters per line used when pretty is true or when printing to IO devices. Set to 0 to force each item to be printed on its own line. If you don’t want to limit the number of items to a particular number, use :infinity . | 80 |
:charlists | When :as_charlists , all lists will be printed as charlists, non-printable elements will be escaped. When :as_lists , all lists will be printed as lists. When the default :infer , the list will be printed as a charlist if it is printable, otherwise as a list. See List.ascii_printable?/1 to learn when a charlist is printable. | :infer |
Example:
Prints:
Here, we can see the first five entries, and the rest are summarized with ...
.
Coloring and Styling Output
Elixir’s inspect options support syntax coloring: very handy when your terminal is full of logs.
Example:
This makes atoms blue, strings green, and numbers red in your console output.
Colors can be any IO.ANSI.ansidata/0 as accepted by IO.ANSI.format/1.
If you want to use the default colors (like what is used in IEx
), you can use IO.ANSI.syntax_colors/0:
Note: The colors are only visible if your terminal supports ANSI colors.
Conditional Inspection
Sometimes you only want to inspect if a certain condition is true: for example, when a debug flag is enabled or when a value crosses a threshold.
Here’s an example with a debug flag:
You can use the function like this:
You can toggle the output by setting config :my_app, :debug, true
or false
.
Capturing Inspect Output Instead of Printing
If you want to get the inspected form of a value without printing it, use inspect/2 (without the IO.
prefix):
This is useful if you want to:
- Write the debug output to a file
- Send it to a logging service
- Include it in an exception message Example:
You can also redirect IO.inspect
output to another device:
Debugging Concurrency and Async Code
When you use IO.inspect
in async code, the output may arrive out of order, making it hard to follow. Adding identifiers or timestamps can help.
Example:
Prints:
Including the PID or a unique request ID in your label helps you trace which output belongs to which process.
Advanced Trick: Inspecting and Pattern Matching in One Go
You can combine pattern matching with inspection to see exactly what’s being matched:
This inspects the user
variable before pattern matching extracts the id
.
You can also place IO.inspect
inside a guard to conditionally print:
In this last example, “Admin user” will only be printed if the user has the role :admin
.
Using dbg/2
for Richer Inspection
Since Elixir 1.14, we have dbg/2, a built-in debugging helper that works like IO.inspect
but also shows the code expression that produced the value.
You can use it like this:
Which prints:
This makes it much easier to understand where in your code the inspected value is coming from, especially when you’re inspecting multiple similar-looking values.
You can also pass options similar to IO.inspect
:
Here are the key differences with IO.inspect
:
- Shows the code expression automatically.
- Output format is slightly more verbose.
- Same return behavior, returns the inspected value, so you can keep it in a pipeline.
- Best for interactive debugging during development.
When IO.inspect
is Not Enough
While IO.inspect
is a fantastic quick-and-dirty tool, there are times when you need more powerful debugging:
- IEx.pry: drops you into an interactive REPL inside the running process.
- :observer.start/0: Erlang’s GUI for monitoring processes, memory, and more.
The trick is to know when to reach for
IO.inspect
and when to switch to one of these other tools.
Setting Default Options for IO.inspect
in IEx
When using IEx
, you can configure the default options for IO.inspect
using the IEx.configure/1 function:
You can easily put this in your .iex.exs file so that it’s applied automatically every time you open the shell.
Creating a Reusable Inspect Helper
Here’s a neat way to wrap IO.inspect/2
into your own helper module with preferred defaults. This way, you can keep consistent inspection output without repeating options everywhere:
You can use it like this:
This way:
- You get pretty printing (
pretty: true
) by default. - Lists and maps are not truncated (
limit: :infinity
). - A label is always shown (
DEBUG
unless overridden). - You can still override any option per call.
- It works fine when it’s in a pipeline.
Handy Visual Studio Code snippets
To make using IO.inspect
faster and more consistent, you can configure editor snippets in Visual Studio Code so you don’t have to type repetitive boilerplate each time.
In Visual Studio Code, open: Code -> Settings… -> Configure Snippets -> Elixir
Add the following snippet definitions:
With these in place, you’ll have short prefixes to insert commonly used IO.inspect/2
patterns:
Prefix | Description |
---|---|
lin | IO.inspect with label |
sin | IO.inspect using selected text as label |
pin | IO.inspect in a pipeline |
pinf | IO.inspect in a pipeline with file and line reference |
cin | IO.inspect clipboard content |
This way, you can quickly drop in debug output with consistent formatting, colors, and labels, without breaking your flow.
Suppose you are transforming a list of user maps and want to check intermediate results inside a pipeline:
With the snippet defined above, you don’t have to type all that. Just type pin
, hit Tab, and you get:
You can immediately type your label (e.g., "After filter"
) and continue coding. This keeps your debugging consistent, colorful, and fast, without breaking your flow.
Best Practices
Now, let’s finally look at a few best practices when using IO.inspect
.
- Use labels liberally: Unlabelled output is harder to parse.
- Limit output: Use
limit
andpretty
for large collections. - Avoid leaving it in production unless intentional.
- Wrap it in helper functions when you want conditional control.
- Tag concurrent output with PIDs, timestamps, or request IDs.
- Credo can be used to detect unintentional calls to
IO.inspect
in a CI/CD pipeline.
Wrapping Up
IO.inspect
may look like a humble debugging tool, but in Elixir, it’s a powerful way to see what’s going on without breaking your code’s flow. By combining it with labels, formatting, conditional output, and process context, you can get precise insights into your program’s behavior, all without leaving your editor.
Happy coding!