Hello everyone, I hope you are all well!
I recently had a WTF moment when working with the Stream
module. I’d like to find out if you are equally surprised or if you think that this should be expected behavior. Here’s the code:
Stream.resource(
fn -> 0 end,
fn
5 ->
IO.puts("source is about to raise")
raise "resource raised"
counter ->
{[counter], counter + 1}
end,
fn _ -> IO.puts("source is finishing") end
)
|> Stream.transform(
fn -> nil end,
fn n, _ ->
IO.puts("received #{n}")
{[n], nil}
end,
fn _ -> IO.puts("transform is finishing") end
)
|> Stream.run()
When I run this code, I would expect to see this output:
received 0
received 1
received 2
received 3
received 4
source is about to raise
** (RuntimeError) resource raised
stream.exs:6: anonymous fn/1 in :elixir_compiler_0.__FILE__/1
(elixir 1.12.1) lib/stream.ex:1531: Stream.do_resource/5
(elixir 1.12.1) lib/stream.ex:880: Stream.do_transform/5
(elixir 1.12.1) lib/stream.ex:649: Stream.run/1
Why? Because when the source raises before emitting element n = 5
, IMO that should be the end of the stream, and no downstream consumers should be executed anymore.
Instead, what I see is the following:
received 0
received 1
received 2
received 3
received 4
source is about to raise
source is finishing
transform is finishing
** (RuntimeError) resource raised
stream.exs:6: anonymous fn/1 in :elixir_compiler_0.__FILE__/1
(elixir 1.12.1) lib/stream.ex:1531: Stream.do_resource/5
(elixir 1.12.1) lib/stream.ex:880: Stream.do_transform/5
(elixir 1.12.1) lib/stream.ex:649: Stream.run/1
As you can see, before the exception is raised, the finishing functions of both the source and the transform step are called. This implies that the exception has been caught somewhere and reraised after these finishing functions are executed.
This behavior has puzzled me when I first encountered it. Do you share my feeling or do you think that this should be the normal behavior, and if so, why?
Thanks!
Max