In Elixir, keyword lists are a common way to pass around options and data. They’re a special type of list composed of two-element tuples where the first element is an atom (the key). Since keyword lists are just lists under the hood, you have several options for combining them, such as using the list concatenation operator ++ or using Keyword.merge/2.

While both approaches can be used to merge keyword lists, they behave differently when it comes to handling duplicate keys. Understanding this difference is crucial when writing code that relies on expected values taking precedence.

In this post, we’ll compare ++ and Keyword.merge/2, highlight their differences with examples, and point to relevant Elixir documentation.

Keyword lists in elixir

A keyword list looks like this:

opts = [timeout: 5000, retries: 3]

Under the hood, this is the same as:

opts = [timeout: 5000, retries: 3]
# => [{:timeout, 5000}, {:retries, 3}]

Elixir allows duplicate keys in keyword lists:

[timeout: 5000, timeout: 1000]
# => [{:timeout, 5000}, {:timeout, 1000}]

The order of keys matters, and functions in the Keyword module are designed to handle this gracefully.

Merging with ++

The ++ operator simply concatenates two lists:

list1 = [timeout: 5000, retries: 3]
list2 = [timeout: 1000]

merged = list1 ++ list2
IO.inspect(merged)
# => [timeout: 5000, retries: 3, timeout: 1000]

Notice that both :timeout entries are preserved. This is valid and sometimes useful, but if you access the value with Keyword.get/2, it will return the first occurrence:

Keyword.get(merged, :timeout)
# => 5000

This behavior can lead to subtle bugs if you're expecting the second :timeout to overwrite the first.

Merging with Keyword.merge/2

If you want later values to overwrite earlier ones, use Keyword.merge/2:

merged = Keyword.merge(list1, list2)
IO.inspect(merged)
# => [timeout: 1000, retries: 3]

Here, the second list’s :timeout key overwrites the first one.

You can also provide a custom function to resolve conflicts:

Keyword.merge(list1, list2, fn _key, val1, val2 -> max(val1, val2) end)
# => [timeout: 5000, retries: 3]

Summary of differences

Operation Keeps All Duplicates? Later Values Overwrite Earlier? Order Preserved?
++ Yes No Yes
Keyword.merge/2 No Yes Yes

Use ++ when you want to preserve all occurrences of keys (e.g., logging, multiple options). Use Keyword.merge/2 when you want to combine options with later values taking precedence.

Learn more

By understanding the nuances of these two approaches, you can choose the right tool for merging keyword lists in Elixir and avoid common pitfalls.