Concurrent Error Handling with Task

In the last lesson, you chained validations with the with statement and, earlier, you used try/rescue to normalize exceptions into {:ok, value} or {:error, reason}. Today, you will combine those ideas with concurrency. You will run multiple operations at the same time with Task and collect their results safely, handling timeouts and crashes without taking down your process.

Isolating Work and Normalizing Failures

We start by defining the work and wrapping it so failures become {:error, reason} tuples. This keeps results consistent across concurrent tasks.

What it does:

  • fetch_data/1 simulates three sources. Two return {:ok, ...}, one raises, and unknown inputs return {:error, ...}.
  • safe_fetch/1 rescues exceptions and converts them to {:error, message}. This prevents a crashing task from propagating an exit to the caller and keeps a uniform result shape.

Notes:

  • Converting exceptions to tuples makes downstream orchestration simple and predictable.
  • You could allow tasks to crash and handle exits, but normalizing here avoids surprises and reduces coupling.
Running Tasks Concurrently and Handling Timeouts and Crashes

Next, you spawn tasks, wait for results with a timeout, and handle all outcomes in one place.

What it does:

  • Task.async spawns three concurrent tasks that call safe_fetch/1. Each task runs independently.
  • Task.yield(task, 5000) waits up to 5 seconds for a result:
    • {:ok, result} contains the exact return from safe_fetch/1 (either {:ok, ...} or {:error, ...}).
    • nil means the task did not finish in time; Task.shutdown then tries to stop it cleanly.
    • {:exit, ...} indicates the task terminated; both shapes are matched for robustness.
  • You collect a list of final, uniform results and print them.
Task.await/2 vs Task.yield/2
  • Task.await/2 raises an exception if the task exits abnormally. Note that Task.async/1 (used to start the task) links the task to the caller by default. This means if a task crashes, it will also crash the parent process unless you trap exits or use Task.Supervisor.async_nolink/3.
  • Task.yield/2 is non-raising: it returns {:ok, result} if the task completes, {:exit, reason} if the task exits, or nil if it times out. This makes it safer for robust error collection, as you can handle all outcomes explicitly without risking a crash in your process.
  • The sample uses yield plus shutdown to ensure you always get a result tuple and can handle timeouts and crashes gracefully, rather than letting exceptions propagate.

Notes:

  • Handling both {:exit, {reason, _stack}} and {:exit, reason} covers different exit formats across Elixir/Erlang.
  • For long-running systems, consider Task.Supervisor and async_nolink to avoid linking tasks directly to the caller.
  • Timeouts are a design choice: pick values based on SLAs and consider per-source timeouts if needed.
Summary and Next Steps

You launched multiple operations at once with Task, contained failures with try/rescue in safe_fetch/1, and collected results reliably using Task.yield/2 plus Task.shutdown/1. You handled normal returns, timeouts, and exits without crashing your process and kept a clean {:ok, value} | {:error, reason} contract.

You are ready to practice turning concurrent work into resilient, easy-to-consume results. Head to the practice section and put these patterns to work.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal