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.
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/1simulates three sources. Two return{:ok, ...}, one raises, and unknown inputs return{:error, ...}.safe_fetch/1rescues 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.
Next, you spawn tasks, wait for results with a timeout, and handle all outcomes in one place.
What it does:
Task.asyncspawns three concurrent tasks that callsafe_fetch/1. Each task runs independently.Task.yield(task, 5000)waits up to 5 seconds for a result:{:ok, result}contains the exact return fromsafe_fetch/1(either{:ok, ...}or{:error, ...}).nilmeans the task did not finish in time;Task.shutdownthen 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/2raises an exception if the task exits abnormally. Note thatTask.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 useTask.Supervisor.async_nolink/3.Task.yield/2is non-raising: it returns{:ok, result}if the task completes,{:exit, reason}if the task exits, ornilif 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
yieldplusshutdownto 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.Supervisorandasync_nolinkto avoid linking tasks directly to the caller. - Timeouts are a design choice: pick values based on SLAs and consider per-source timeouts if needed.
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.
