Preamble
This Wiki is intended to be a community-maintained (see the Contribution Guidelines if interested) resource of common “gotchas” (unexpected issues) that beginners to Elixir might experience.
The main intended audience is programmers used to other languages, who come to Elixir and find themselves confused by some particularities of the language and its ecosystem. We also hope it can be used by experienced Elixir programmers as a tool for answering questions and guiding beginners - a quick link to have handy.
Please note that we aren’t aiming to write for beginners to programming. For that, you can check out Joy of Elixir.
We’re also not aiming to replace any official documentation. Beginners to Elixir should start with the Elixir Getting Started guides (which are excellent).
Table of Contents
- My list of integers is printing as a string (charlists)
- I thought Elixir data was immutable, but I’m allowed to reassign variables!
- I can’t reassign variables inside an
if
block or a loop - Sometimes people write a
def
without ado
block - I can’t pattern match on an empty map
- Using String.to_atom on untrusted input
- Trying to pattern match on a keyword list
Gotchas and Common Issues
My list of integers is printing as a string (charlists)
In IEx or the output of IO.inspect/2
, if you have a list of integers where all the integers are printable ascii code points, they will be printed as a charlist. For example:
[6, 7, 8]
is rendered as[6, 7, 8]
since6
is not a printable ascii code point[7, 8, 9]
is rendered as'\a\b\t'
- Both are equivalent:
'\a\b\t' == [7, 8, 9]
- Both are equivalent:
[99, 97, 116]
is rendered as'cat'
It’s important to understand that the list of integers and the charlist are exactly equivalent. There’s no reason to and no way to “convert” between them. But you can get your IEx shell to represent them differently on inspect.
More details
- Why it is this way
- By default IEx will use the
IO.Inspect
protocol to pretty-print all results- If the list is a valid printable charlist, then IEx will print it as a charlist
- By default IEx will use the
- Official Elixir FAQ entry: https://github.com/elixir-lang/elixir/wiki/FAQ#4-why-is-my-list-of-integers-printed-as-a-string
- What to do
-
For the current IEx session, you can use
IEx.configure/1
e.g.IEx.configure(inspect: [charlists: false])
, or configure it globally with a .iex.exs fileiex(1)> [7, 8, 9] '\a\b\t' iex(2)> IEx.configure(inspect: [charlists: false]) :ok iex(3)> [7, 8, 9] [7, 8, 9]
-
Add a
0
to the end to ensure that all the code points are not printable:iex(1) [7, 8, 9, 0] [7, 8, 9, 0]
-
Understand that IEx is just pretty-printing your result - the values are exactly equivalent
-
Understand charlists vs strings
-
- Examples
- https://elixirforum.com/t/how-to-use-a-list-with-non-negative-integers-without-making-it-a-charlist/26082
- https://elixirforum.com/t/7-8-9-in-iex-returns-a-b-t-why/29421
- https://elixirforum.com/t/unexpected-keyword-merge-behavior-elixir-1-9-4/27596
- Elixir lists interpreted as char lists - Stack Overflow
I thought Elixir data was immutable, but I’m allowed to reassign variables
The following is valid Elixir code:
a = 1
a = 3 # Changes the binding of `a`
IO.puts(a) # Prints "3"
While it looks like this Elixir code is mutating data, it is actually rebinding the variable a
.
More details
- The behavior of Elixir here is different than Erlang to be more pragmatic and avoid requiring the programmer to have variables like a1, a2, a3 if they want to change the value of a binding
- Further reading:
- Related to trying to change the value of a binding in a loop, see next point
- Examples
Why do we prepend to lists?
You may have learned other languages, where they build lists by appending, while in Elixir we usually prepend. But why? It’s not just style. In Elixir, and most other languages that use immutable data, prepending to a list is much more efficient.
More details
Under the hood, most implementations of lists boil down to a classic singly linked list. So, appending with mutable data would mean modifying the next-pointer of the previously last node to point at the new last node.
But with immutable data, you can’t do that! So you have to create a new node, pointing to the added node, and throw away the old one. Any existing references to this, must be updated… such as the next-pointer in that previously second-to-last node. However, the same condition applies, recursively, all the way back to the beginning of the list.
So, appending to a list with immutable data means you have to reconstruct the entire list every time. Since this must be done for every node you append, it takes the time to process the whole list from linear (proportional to the length) to quadratic (proportional to the square of the length).
By contrast, if you prepend a node, whether the data is mutable or not, you simply create it pointing to the old first node. Nothing more needs to be done. So, the time to process one node is constant, so the time to process the whole list is once again linear.
I can’t reassign variables inside an if
block or a loop
a = 1
if a > 0 do
a = 3
end
IO.puts(a) # prints "1"
This is related to the previous section. The main takeaway is: rebinding a variable only applies to the current block scope.
More details
Here’s a more realistic example of this gotcha
vegetables = ["radish", "celery", "carrot"]
# We want this to become a list of vegetables starting with the letter "c"
c_vegetables = []
# We'll loop over `vegetables`
# and try to concat each one that starts with "c" to the above list
Enum.each(vegetables, fn vegetable ->
if String.starts_with?(vegetable, "c") do
c_vegetables = Enum.concat(c_vegetables, [vegetable])
# Inside this `if` block, `c_vegetables` will be rebound to one-item list
# But it doesn't change the value of `c_vegetables` outside this block
IO.inspect(c_vegetables) # Prints either `["celery"]` or `["carrot"]`
end
end)
IO.inspect(c_vegetables) # This still has the value `[]`
Instead you should reassign the value of c_vegetables
itself
vegetables = ["radish", "celery", "carrot"]
# Enum.filter docs: https://hexdocs.pm/elixir/Enum.html#filter/2
c_vegetables = Enum.filter(vegetables, fn vegetable ->
String.starts_with?(vegetable, "c")
end)
IO.inspect(c_vegetables) # Prints out ["celery", "carrot"]
- To understand what is happening you should understand how scoping works in Elixir.
- This was actually allowed up to Elixir 1.6(?), and gave a warning up until Elixir 1.9(?)
Further reading:
- https://elixir-lang.org/getting-started/case-cond-and-if.html#if-and-unless
- https://elixir-lang.org/getting-started/try-catch-and-rescue.html#variables-scope
- https://elixir-lang.readthedocs.io/en/latest/technical/scoping.html
Examples:
Sometimes people write a def
without a do
block
i.e. first line of this snippet:
def foo(result, acc)
def foo(:ok, _acc) do
:ok
end
def foo(:error, acc) do
{:error, acc}
end
This is called a function head, or a bodyless function clause. If a function with default values has multiple clauses, it is required to create a function head for declaring defaults.
More details
Further reading:
- https://stackoverflow.com/questions/41356447/whats-the-need-for-function-heads-in-multiple-clauses
- https://bordeltabernacle.netlify.app/elixir/notes-on-elixir-bodyless-functions/
- https://elixir-lang.org/getting-started/modules-and-functions.html#default-arguments
Examples:
I can’t pattern-match on an empty map
Why does %{}
match %{foo: true}
? It’s because an empty map matches any map, not just empty maps.
These match successfully:
%{} = %{}
%{} = %{a: 1} # this doesn't error out!
Here’s an example of trying to pattern-match to an empty map in a function declaration.
def my_function(%{}) do
:unexpected
end
def my_function(map) do
map
end
# This is matched by the first function clause above!
my_function(%{a: 1})
# => :unexpected
If you want to match only to specifically an empty map, you’ll either need to use either == %{}
or a different guard clause.
More details
If Elixir/Erlang didn’t work this way then whenever you wanted to pattern match a map you would have to provide all of the keys, which would not be very ergonomic.
Instead in some cases you may want to use a guard instead:
def my_function(options) when options == %{} do
# Handle case when no options are passed
end
Using String.to_atom on untrusted input
It is important to not call String.to_atom/1
on untrusted input from the network or user
More details
The documentation for String.to_atom/1
states:
Warning: this function creates atoms dynamically and atoms are not garbage-collected. Therefore, string should not be an untrusted value, such as input received from a socket or during a web request. Consider using to_existing_atom/1 instead.
Examples:
- https://elixirforum.com/t/key-not-found-map/4506
- https://elixirforum.com/t/help-me-make-this-more-idiomatic/39633
Similarly you need to ensure that you don’t use :erlang.binary_to_term/2
on untrusted input as well:
https://blog.ispirata.com/how-to-destroy-your-application-using-erlang-binary-to-term-1-575ff7d05333
This is the same reason to generally avoid Jason.decode/2
with [keys: :atoms]
and Poison.Parser.parse/2
with %{keys: :atoms!}
Trying to pattern match on a keyword list
Avoid:
def my_function(name: name, age: age) do
# do something with `name` and `age`
end
my_function(age: 18, name: "John")
# Results in a `FunctionClauseError`
More details
Instead you should use functions like Keyword.get/3
or Keyword.fetch!/2
:
def my_function(opts) when is_list(opts) do
name = Keyword.fetch!(opts, :name)
age = Keyword.fetch!(opts, :age)
# Or use `Keyword.get/3` if name or age are optional
end
Or pass options as a map:
def my_function(%{name: name, age: age}) do
# do something with `name` and `age`
end
By pattern matching on a keyword list, you are stating that only a single ordering is allowed (i.e. that :name
must be the very first element in the list in our example)
Contribution Guidelines
Guidelines
The primary target of this wiki post is to provide an easily digestible list of common gotchas or issues that beginners in Elixir often encounter. It is also a central place that people can link to an individual gotcha to help explain a specific issue in depth instead of having to type up a response in every chat thread or forum post.
Each gotcha should be commonly run into, so if it is something that does not come up often it’s best to not include it so that this wiki is easily understandable and not overwhelming to a beginner.
When editing, please try to avoid changing the titles of a gotcha because it will break any links that someone has made to the gotcha. And of course, all of the ElixirForum policies apply as well: https://elixirforum.com/faq
Future ideas for gotchas
Runtime vs Compile time
- Why
init/1
inPlug
is not called for each request in the Phoenix? - When
@foo bar()
will be evaluated.
Maps and function calls
Accessing a map value is a function call (Intermediate)
a = %{val: 42}
a.val
# is the same as `a.val()`
a.foo
# is equivalent to
case a do
%{foo: val} -> val
mod -> mod.foo()
end
Trying to access a map when it isn’t a map (include?)
Not knowing how to interpret trying to call functions on {:ok, term} tuples
# Suppose that fetch_user/0 is a function that returns:
# {:ok, %{name: "john", age: 18}}
# But suppose the programmer is expecting simply %{name: "john", age: 18}
a = fetch_user()
a.age
** (ArgumentError) you attempted to apply :age on {:ok, %{age: 18}}. If you are using apply/3, make sure the module is an atom. If you are using the dot syntax, such as map.field or module.function(), make sure the left side of the dot is an atom or a map
:erlang.apply({:ok, %{name: "john", age: 18}}, :age, [])
Note: This probably isn’t super important to include because the error message is pretty good now (although it could probably be improved slightly, the error message doesn’t make it clear that you are trying to call a function, ideally it would also talk about how map access is actually a function call)
# On OTP 20 this was even more confusing because of tuple calls
iex(1)> a = {:ok, %{age: 20}}
{:ok, %{age: 20}}
iex(2)> a.age
** (UndefinedFunctionError) function :ok.age/1 is undefined (module :ok is not available)
:ok.age({:ok, %{age: 20}})
Configuration evaluation time
- When is
config/config.exs
evaluated - Why I cannot configure
:kernel
optionsconfig/config.exs
in development is evaluated after VM is started, so it cannot configure already started applications (like:kernel
or:stdlib
)config/runtime.exs
cannot configure above applications, because of the same reason (unless:reboot_systemd_after_config
is set totrue
)
- Compile time vs runtime configuration
Type related gotchas
true
,false
, andnil
are atoms- Module names are atoms
Foo == :"[Elixir.Foo](http://elixir.Foo)"
(alsoFoo == Elixir.Foo
, theElixir
prefix is to avoid naming conflicts on case insensitive filesystems like default setting of NTFS on Windows and default settings of APFS on macOS) foo == []
can be much faster thanlength(foo) == 0
[]
has smaller representation when used with:erlang.term_to_binary/1
`:` vs `=>` in map syntax (may syntactic sugar)
Both are valid, :
creates an atom, =>
can be used for any term
Mixing strings and atoms in maps
Especially a problem in Phoenix controllers, and controller tests
mix deps.update doesn't always update dependencies
If your mix.exs file is restrictive, then mix deps.update will run, but won’t update the dependency that you specified. To fix this you need to update the version requirement of the dependency in mix.exs
Needing a . to call a function in a variable
iex(1)> myadd = fn num -> num + 1 end
#Function<44.79398840/1 in :erl_eval.expr/5>
iex(2)> myadd.(1)
2
Single-line Function syntax
-
Why does
def myfun, do: 42
have a comma?
This is because the multi-line version is syntactic sugar for:def add(a, b) do a + b end # Is equivalent to def(add(a, b)) do (a + b) end # And (need to double check that this works) def(add(a, b), do: (a + b))
-
Why there is
.
when calling anonymous functions (likefoo.()
)- To differentiate between the local functions and variables
Post about Jose about parens
Understanding where Phoenix path helpers come from
By default up to Phoenix 1.4(?) the path helpers were always imported when you run use MyAppWeb
in Phoenix 1.5 the MyAppWeb
__using__
macro includes an alias of the MyAppWeb.Router.Helpers
module which is an auto-generated module.
= vs <- in with clauses
When using the with
statement, if you want the non-matched value to be returned or else
clauses to be executed, you need to use <-
instead of =
when pattern matching. Using =
is valid but has a different meaning: the failed pattern match will raise a MatchError
.