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.






















