pierrelegall
Add `List.first!/1` and `List.last!/1`
Problem
Currently, List.first/2 and List.last/2 return a default value (or nil) when the list is empty. However, there are cases where an empty list represents a bug in the program, and silently returning nil can lead to confusing errors later in the code.
Other modules in Elixir provide bang variants for similar situations:
Map.fetch!/2raises when a key is not foundList.keyfind!/3raises when a key is not foundEnum.fetch!/2raises when an index is out of bounds
Proposal
Add List.first!/1 and List.last!/1 functions that raise an ArgumentError when called with an empty list.
List.first!([1, 2, 3]) #=> 1
List.first!([]) #=> ** (ArgumentError) trying to get the first element of an empty list
List.last!([1, 2, 3]) #=> 3
List.last!([]) #=> ** (ArgumentError) trying to get the last element of an empty list
Use case
This is useful when you have a list that should never be empty at a certain point in your code. Using the bang variant makes the intent explicit and provides a clear error message if the assumption is violated, rather than propagating nil through the system.
How to do without it today?
1. Pattern matching (most idiomatic for first element)
[head | _] = list
# raises MatchError if empty
2. hd/1 for first element
hd(list)
# raises ArgumentError: argument error
3. Enum.fetch!/2
Enum.fetch!(list, 0) # first
Enum.fetch!(list, -1) # last
4. Case expression
case list do
[head | _] -> head
[] -> raise ArgumentError, "empty list"
end
However, none of these are ideal:
hd/1exists for first, but there’s no equivalent for lastEnum.fetch!(list, -1)works but is O(n) twice (once to get length, once to traverse) and less readable- Pattern matching doesn’t work for last element without reversing first
- Case expressions are verbose for a simple operation
List.last!/1 fills a real gap. List.first!/1 provides symmetry and a clearer error message than hd/1.
Why ArgumentError?
The implementation raises ArgumentError. Here’s how it compares to similar functions:
| Function | Error raised | Reason |
|---|---|---|
hd([]) |
ArgumentError |
Invalid argument (not a nonempty list) |
Map.fetch!(%{}, :a) |
KeyError |
Key not found in map |
Enum.fetch!([], 0) |
Enum.OutOfBoundsError |
Index out of bounds |
List.keyfind!([], :a, 0) |
KeyError |
Key not found |
Enum.random([]) |
Enum.EmptyError |
Empty enumerable |
Arguments for ArgumentError
- Consistency with
hd/1: The closest existing function ishd/1, which raisesArgumentErrorfor an empty list.List.first!/1is essentially a more readablehd/1. - Semantic fit: The error is about passing an invalid argument (an empty list where a non-empty one is required). This is the textbook definition of
ArgumentError. - Module boundary:
Listis notEnum— usingEnum.EmptyErrorinListcould be seen as a layer violation.
Alternative: Enum.EmptyError
Enum.EmptyError exists for “operation on empty collection” errors and is used by Enum.random/1, Enum.min/1, Enum.max/1, etc. It’s semantically closer to this situation.
Conclusion
Both are defensible. ArgumentError was chosen for consistency with hd/1 and existing List module conventions.
I already have a PR done locally.
Most Liked
pierrelegall
The PR has been merged into Elixir main branch.
garrison
Ironically Erlang’s :lists.last() actually raises.
josevalim
We have recently discussed it here: Typing lists and tuples in Elixir - The Elixir programming language
The idea is that hd will require a non-empty list to be given as argument (so in statically typed code you won’t get a runtime failure), while a potential List.first! and List.last! will imply a runtime error if an empty list is given (hence the bang). Given we were already planning to have those, please go and submit a PR, although I’d say it is also desirable to speed up Enum.fetch!(list, -1).
I’d say it is redundant to have List.first (or similar) that return a tuple given you can directly match on the list instead:
case List.fetch_first(list) do
{:ok, first} -> first
:error -> ...
end
vs
case list do
[first | _] -> first
[] -> ...
end
The second case is simpler, faster and clearer (IMO). So the List.first should either return a default (so you can skip the case) OR raise. The tuple case is not valuable.
Popular in Proposals: Ideas
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









