Task.await wont exit task on timeout in iex

Hello everyone, I am having some issues understanding exits and Task.await

From the elixir documentation about Task.await/2

If the timeout is exceeded, then the caller process will exit. If the task process is linked to the caller process which is the case when a task is started with async , then the task process will also exit.

It says “caller process will exit”, what does “exit” mean here? I think it means it will call Kernel.exit/1 function.

According to the documentation exit/1 will

Stops the execution of the calling process with the given reason

When I try to call exit from an iex session why does it not kill the current process, I expected it to kill the iex process but that’s not the case as seen here

Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> self()
#PID<0.111.0>
iex(2)> exit(:boom)
** (exit) :boom

iex(2)> self()     
#PID<0.111.0>

Due to this when playing with Task.await on iex the caller process is not killed on exit so the task continues to run, check this sample code…

defmodule TaskTest do
  def test_await() do
    task_ref = Task.async(&perform/0)
    return_value = Task.await(task_ref)
    IO.puts "I am printed after await, task returned: #{inspect(return_value)}"
  end

  def perform(sleep_time \\ 10_000) do
    IO.puts "Sleeping for #{sleep_time}ms zZz"
    Process.sleep(sleep_time)
    IO.puts "Done sleeping"
    :return_value_for_caller
  end
end

Now if I call TaskTest.test_await in iex this is what I get

iex(3)> t = TaskTest.test_await
Sleeping for 10000ms zZz
** (exit) exited in: Task.await(%Task{owner: #PID<0.111.0>, pid: #PID<0.131.0>, ref: #Reference<0.979233406.103874576.149424>}, 5000)
    ** (EXIT) time out
    (elixir 1.13.0) lib/task.ex:807: Task.await/2
    iex:4: TaskTest.test_await/0
Done sleeping

As you can see the task is not exiting since the caller process(iex shell process) is not exiting on await timeout.
And it continues to run and prints Done sleeping.

Now instead of the iex process if the caller process is different then it works as expected like…

iex(3)> {:ok, pid} = Task.start(&TaskTest.test_await/0)
Sleeping for 10000ms zZz
{:ok, #PID<0.133.0>}
iex(4)> 
16:42:41.305 [error] Task #PID<0.133.0> started from #PID<0.111.0> terminating
** (stop) exited in: Task.await(%Task{owner: #PID<0.133.0>, pid: #PID<0.134.0>, ref: #Reference<0.979233406.103874575.148817>}, 5000)
    ** (EXIT) time out
    (elixir 1.13.0) lib/task.ex:807: Task.await/2
    iex:4: TaskTest.test_await/0
    (elixir 1.13.0) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
    (stdlib 4.0.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Function: &TaskTest.test_await/0
    Args: []
 
nil
iex(5)> Process.alive?(pid)
false

Here the caller process is another task, when Task.await exceeds its timeout it exits the caller process(the task started with Task.start) and hence the linked task also exits and never prints Done sleeping.

I want to understand why the behaviour is different in iex? This leads to confusion when I am trying to play around with Task in iex. Is my understanding correct here?

Can someone help regarding this?

1 Like

exit(:reason) calls :erlang.exit/1 which just raises an exception - it should terminate the calling process, but seems that IEx has special handling for exceptions. What you really want is to send a real exit signal with Process.exit/2:

iex> raise ArgumentError, "foo"
** (ArgumentError) foo

iex> exit :boom
** (exit) :boom

iex> Process.exit(self(), :boom)
** (EXIT from #PID<0.116.0>) shell process exited with reason: :boom

Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
2 Likes

seems that IEx has special handling for exceptions.

That answered my question. Thanks a lot.

Task.await is using :erlang.exit/1 as seen here which just raises an exception and since IEX has special handling for exceptions (maybe here) it does not exit the IEX process.

This answers why awaiting tasks in IEX won’t lead to the task exiting on timeout.

1 Like