pierrelegall
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.
Trending in Proposals: Ideas
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
garrison
Ironically Erlang’s
:lists.last()actually raises.sodapopcan
Tangentially, I also find
List.wrap(nil)returning[]extremely non-intuitive. I sorta get it but also not. Is there precedent?pierrelegall
We can found in the source, on
main:The behavior was, on
v1.0:Pushing breaking changes is sadly often not reasonable
LostKobrakai
This is quite common around the maybe monad, where no value maps to
[]and a value to[value]. In some languages[]andnilwould even be considered the same value. Erlang internally represents[]as a datatype called nil. … but more likely in ruby you haveArray(nil) => [].derek-zhou
So that I can write
Instead of:
sodapopcan
I actually was not aware of that
Array(nil)in Ruby did that, either because I never came across it but more likely (since I usedArray()a ton in Ruby) that I’d explicitly check fornil. But that is certainly good precedent for Elixir. Otherwise ya, that’s all fair. I’ve always thought ofnilas living outside of the “monadic” idioms but that makes sense. Thanks for the answer!krasenyp
Also in Common Lisp,
niland(), the empty list, can be used interchangeably.nilis also a generalised boolean, standing forfalse.lud
What was the point of first/0 and last/0 ?
pierrelegall
The point is
List.first/1andList.last/1do not return an:errorbut anilif the given list if empty, which is error prone.jswanner
I haven’t found
List.first/0orList.last/0in the repo’s history…