
Dialyzer (DIscrepancy AnaLYZer for ERlang programs) is a powerful static analysis tool that helps developers identify potential issues in their Elixir code without executing it. It excels at finding type mismatches, unreachable code, and unnecessary functions through sophisticated flow analysis.
In part one of this two-part series, we'll first get to grips with the basics of Dialyzer. In part two, we'll examine more advanced use cases.
An Introduction to Dialyzer for Elixir
In the world of dynamic languages like Elixir, type-related issues traditionally only surface during runtime, making them harder to catch during development. Dialyzer bridges this gap by analyzing your code's type specifications and usage patterns to detect potential problems before they reach production.
When integrated into your development workflow, Dialyzer provides several key benefits:
- Enhanced Code Reliability: Catch type-related bugs early in the development cycle.
- Better Documentation: Type specifications serve as living documentation and provide useful hints with IDE integration.
- Improved Developer Productivity: Identify issues before they cause runtime errors.
- Safer Refactoring: Get immediate feedback about type-related changes.
Setting Up Dialyzer for Elixir
While Dialyzer itself comes with Erlang, the recommended way to use it in Elixir projects is through dialyxir
, a mix task that makes Dialyzer more convenient to use.
First, add dialyxir
to your project's dependencies in mix.exs
:
def deps do [ {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, ] end
After adding the dependency, install it by running:
mix deps.get
Now you can run Dialyzer with:
mix dialyzer
On the first run, Dialyzer will build a Persistent Lookup Table (PLT), which contains information about your project and its dependencies. This process might take several minutes, but it's a one-time operation. The PLT will be cached and reused in subsequent runs.
The output will look something like this:
Compiling PLT .dialyzer_plt Starting Dialyzer Total errors: 0, Skipped: 0, Unnecessary Skips: 0 done in 0m1.34s done (passed successfully)
If Dialyzer finds issues, it will display warnings like:
lib/example.ex:2:invalid_contract The @spec for the function does not match the success typing of the function. Function: Example.hello/0 Success typing: @spec hello() :: :error
Although Dialyzer can infer types from your code, adding explicit type specifications helps it perform a more thorough analysis. We'll explore how to write effective type specifications in the next section.
Types and Type Specifications in Elixir
Type specifications serve multiple purposes in Elixir. Beyond documentation, they provide Dialyzer with the information needed to perform sophisticated flow analysis. As someone who's worked extensively with Elixir in production, I've found that well-designed type specifications:
- Act as executable documentation
- Enable early bug detection
- Make refactoring safer
- Improve IDE integration
Adding Typespecs to Your Elixir Code
Let's start with the basics and progress to more complex scenarios. There are two main type attributes you'll use frequently:
@type name :: <<type scpecification>>
defines a type with the specified name.@spec function_name(argument_types) :: return_type
defines a function specification.
There are several built-in types like any()
, none()
, atom()
, map()
, and integer()
.
It is also possible to compose types using list(type)
, nonempty_list(type)
, etc.
You can use types defined in other modules as well.
It is quite common for data structures to define their types named t
.
In fact, several Elixir data structures already define types that can be used in your custom typespecs, like String.t()
and Range.t()
.
Finally, we can use almost any literal as a type that restricts the allowed values to be like the literal.
For example, 1
, 1..10
, {:ok, type}
.
We'll cover advanced attributes to create other special types in the second and final part of this series.
For now, here's an example of using some typespecs in practice:
defmodule UserAccount do @type status :: :active | :inactive | :suspended @type role :: :admin | :user @spec create_user(String.t(), status(), role()) :: {:ok, map()} | {:error, String.t()} def create_user(name, status, role) do # Implementation end @spec is_active?(status()) :: boolean() def is_active?(status), do: status == :active end
In this example:
- We define custom types
status
androle
using union types - The
create_user
function specification ensures that arguments are of the correct types - Dialyzer will catch type mismatches, like passing a number instead of a string for a name
We'll explore more advanced type attributes like @opaque
in the next post.
Understanding Common Dialyzer Warnings
Now that we have Dialyzer set up and running, let's explore the most common warnings you'll encounter and learn how to address them effectively.
1. Match Errors
Dialyzer detects unused pattern matches by analyzing your types:
defmodule Example do def ok() do unmatched(:ok) end defp unmatched(:ok), do: :ok defp unmatched(:error), do: :error end
The pattern can never match the type. Pattern: :error Type: :ok
2. Redundant Guard Clauses
This warning shows Dialyzer's ability to detect impossible conditions:
defmodule Example do def to_string(%{} = a) when is_binary(a) do inspect(a) end end
# lib/example.ex:2:31:guard_fail The guard clause can never succeed.
Here, we're pattern matching on %{}
(a map) but then using an is_binary/1
guard — these conditions can never be true simultaneously since a value cannot be both a map and a binary.
Such contradictions often indicate logical errors in your code's flow control.
Remove the contradictory guard or fix the pattern matching based on your intended behavior.
3. Unmatched Returns
This warning is particularly important for maintaining robust error handling:
defmodule Example do def ok() do :rand.uniform(100) |> validate() :ok end defp validate(n) when n < 50, do: :ok defp validate(n) when n > 50, do: {:error, "too big"} end
lib/example.ex:unmatched_return The expression produces a value of type: :ok | {:error, <<_::56>>} but this value is unmatched.
Dialyzer has detected that the function returns a union type (:ok | {:error, binary()}
) that we are ignoring.
In Elixir, it's a best practice to handle all possible return values, especially errors.
To fix this, you should:
- Pattern match on the result and handle both cases, or
- Use
with
expressions for cleaner error handling, or - Explicitly discard the result using
_ =
if that's truly intended
4. No Local Return
This indicates that your function never returns normally — it either raises an exception or runs indefinitely:
defmodule Example do def ok() do Enum.each([1, 2, 3], fn _ -> raise "error" end) end end
lib/example.ex:no_return The created anonymous function only terminates with explicit exception.
This is common in placeholder code, but in production, you should ensure your functions have proper return paths.
If the function is meant to be raised, specify no_return()
in the typespec:
@spec do_something() :: no_return()
In addition to obvious no-returns, Dialyzer often also raises this as a side-effect of other errors that lead to a guaranteed exception.
For example, the following code tries to pass age
as a string when it's declared type is an integer, leading to a Dialyzer no_return
error as a side-effect of the call
error.
defmodule User do @type t :: %__MODULE__{name: String.t(), age: integer()} defstruct [:name, :age] @spec get_age(User.t()) :: integer() def get_age(user), do: user.age end defmodule Example do def do_something() do User.get_age(%User{name: "User", age: "20"}) end end
lib/example.ex:10:7:no_return Function do_something/0 has no local return. lib/example.ex:11:18:call The function call get_age will not succeed.
This can sometimes also happen when a library has incorrect typespecs or complex types that Dialyzer has trouble inferring.
In such cases, it can be desirable to skip Dialyzer warnings.
This can be achieved by using a special @dialyzer
attribute to disable warnings, specifying a tuple with the warning type and the fuction/arity.
defmodule Example do @dialyzer {:no_return, do_something: 0} def do_something() do User.get_age(%User{name: "User", age: "20"}) end end
And that's it for this part of our two-part series!
Wrapping Up
Getting started with Dialyzer might seem daunting at first, but the benefits are worth the initial setup time. As you've seen, it can catch many common issues before they reach production.
If you already have a large project without Dialyzer, the trick is to start small. First:
- Add Dialyzer to your development dependencies
- Begin with a single module
- Add type specifications gradually
In our next and final part of this series, we'll dive deeper into advanced type specifications, explore sophisticated troubleshooting techniques, and learn how to handle complex scenarios that you might encounter as your codebase grows.
We'll also look at how to effectively configure Dialyzer for larger projects and establish team-wide practices for maintaining type specifications.
Until next time!
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!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our Elixir Alchemy newsletter and never miss an article again.
- Start monitoring your Elixir app with AppSignal.
- Share this article on social media
Most popular Elixir articles
Using Mnesia in an Elixir Application
Learn about Mnesia and how to use it in an Elixir application.
See moreBuilding Compile-time Tools With Elixir's Compiler Tracing Features
Check out one of Elixir's latest features—compiler tracing—and find out why you should consider using it.
See moreMonitoring the Erlang VM With AppSignal's Magic Dashboard
In this post, we walk through monitoring the Erlang VM with the metrics automatically shown in AppSignal's Magic Dashboard.
See more

Pulkit Goyal
Our guest author Pulkit is a senior full-stack engineer and consultant. In his free time, he writes about his experiences on his blog.
All articles by Pulkit GoyalBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!
