Elixir Gotchas and Common Issues Wiki


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

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] since 6 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]
  • [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

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

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
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"]`

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")

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:


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

def foo(:error, acc) do
  {:error, acc}

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:


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

def my_function(map) do

 # 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

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.


Similarly you need to ensure that you don’t use :erlang.binary_to_term/2 on untrusted input as well:


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


def my_function(name: name, age: age) do
  # do something with `name` and `age`

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

Or pass options as a map:

def my_function(%{name: name, age: age}) do
  # do something with `name` and `age`

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


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 in Plug 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}
# is the same as `a.val()`


# is equivalent to

case a do
  %{foo: val} -> val
  mod -> mod.foo()

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()

** (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 options
    • config/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 to true)
  • Compile time vs runtime configuration
Type related gotchas
  • true, false, and nil are atoms
  • Module names are atoms Foo == :"[Elixir.Foo](http://elixir.Foo)" (also Foo == Elixir.Foo, the Elixir 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 than length(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)
Single-line Function syntax
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.


Big thanks to Andy Tran (@a8t) and Łukasz Jan Niemier (@hauleth) who helped create the initial version of this wiki :heart:


Good stuff!

PS. Some of the links don’t render properly (see “This is the same reason to generally avoid”).


Perhaps linking to select forum items in this category would be beneficial in this Wiki

Latest Learning Resources/Guides/Tuts/Tips/Info topics - Elixir Programming Language Forum (elixirforum.com)


Thanks! Fixed! It seems the markdown didn’t export from Notion quite as cleanly as I thought.

That’s a great idea! It’s a wiki so you (or anyone) should feel free to link any resources that make sense :+1:

1 Like

Nice wiki. Is linking to Stack Overflow appropriate, do do you prefer to stick to forum / official sources?

1 Like

Solid list. Add a “did you remember to rollback that migration in the test environment?” and I think you’ll be complete.


FWIW, if linking to Stack Overflow is inappropriate, hereby I grant any right to copy-paste my answers from there without a necessity to link and/or somehow refer to it, nor even mention the original source.

I am to explicitly clarify this because there are many answers provided by me, some are very detailed and I hope they are of some value, and I really don’t care about anything but the quality of this wiki. Redundant references would just perplex readers.


I think linking out to StackOverflow is fine :+1:

1 Like

My personal gotchas list (let me know which ones are good candidates to be added to the wiki):

  1. is_atom(nil) returns true (i.e. nil is an atom)
  2. nil[:anything] returns nil (can be useful sometimes, but most times it would surprise me)
  3. Config.config/{2,3} would run a keyword deep merge
    It surprised me when I set the same config in both config.exs and prod.exs
    For example: config :app, :key, values: [v1: 1] + config :app, :key, values: [v2: 2] = config :app, :key, values: [v1: 1, v2: 2]
  4. ExUnit.run would not load any modules (not even the module it’s intended to test, as ExUnit doesn’t know this intention)
  5. function_exported?/3 won’t load the module
    (leads to different behaviour in test environment and compiled environment due to 4)
  1. true and false are also atoms :slight_smile:
  2. any.anything will fail if the key does not exists, while any[:anything] will not, but return nil

We never got around to it, but we could expand this out into a full ‘Glossary & Gotchas’ section if you think there would be enough topics to warrant dedicated threads Jason.

Here’s the initial thoughts for what we had in mind for the Glossary section: https://elixirforum.com/t/do-you-think-we-need-a-glossary-section/12988

The good thing about dedicated threads is that it’s easier for people to post responses/comments, easier to link to and of course more likely to be picked up and indexed by search engines. If this is something you’d like to do/take charge of, just let me know :023: (A single thread like this is a good option too mind.)

any.anything will fail if the key does not exists, while any[:anything] will not, but return nil

Yes, this is true if any is a map, but nil is an atom.

In the nil[:anything] case, nil is a special atom because there is a special case in Access.get for nil:

I believe it was introduced in Allow the access protocol on nil, and always return nil · elixir-lang/elixir@cc68813 · GitHub to allow nested get_in(data, [...]) to return nil if any intermediate step to return nil
This special case is useful when it’s used in get_in/3,
but when you expect something to be a map, and calls map[:anything], it would return nil when map is nil.

It should be a rare case, but it did surprise me once before


One thing that was surprising for me when I was first introduced to the language was the shorcut syntax for creating anonymous functions.

I was like: What does &{:ok, &1} actually mean? It is the same as fn argument -> {:ok, argument} end

One of the places I first saw this syntax (way back in time) was in Phoenix source and it had something like &"Something went wrong because of #{inspect(&1)}".

iex> !![] == !!%{} == !!0 == !!{} == true
1 Like

Erlang default strings are charlists eg 'this string is a charlist'. This is an Erlang style list aka a linked list not an array.

Elixir default strings are binary eg "this string is a binary".

A binary is not an array of individual chars like a C string. It is a ‘UTF-8 encoded binary’ which describes exactly what it is but makes it no less confusing for those coming from other languages where a string is an array of chars.

1 Like

By the way, Erlang’s 'foo bar' is Elixir’s :"foo bar". They are atoms.

BTW, you can call Elixir functions in Erlang. For example, the way of calling Elixir function String.split("a, b, c", ", ") in Erlang is

'Elixir.String':split(<<"a, b, c">>, <<", ">>)

By the way, Erlang’s 'foo bar' is Elixir’s :"foo bar". They are atoms.

No. Your second example is an atom but the first is not.

An atom in Erlang is just atom. This translates to :atom in Elixir which is also equivalent to :"atom".

If strings were atoms you would run out of memory really quickly as they are not garbage collected and are a bit like a global enum (like an enum in C style languages. Enum in Elixir is a slightly different thing).

Erlang also supports atom literals that can’t be written in the format of [a-z]\w*. Just put whatever you want inside ' ', and you have an atom.

Erlang Elixir Type
foo_bar :foo_bar atom
'foo bar' :"foo bar" atom
"foo bar" 'foo bar' charlist (list of Unicode code points)
<<"foo bar">> "foo bar" binary
'Elixir.Foo.Bar' Foo.Bar
atom (usually used as an Elixir module name)
$x ?x integer (Unicode code point)

The first is an atom in Erlang. See my previous reply.

I agree that strings created at run-time should not be converted to atoms, but Erlang’s 'foo bar' is a compile-time literal, so even if it was a charlist or a binary (it’s neither), it would be no danger at all keeping it away from garbage collection.