We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Use `Task.Supervisor` instead of bare `Task.async`
almirsarajcic
Bare Task.async creates linked tasks that can crash your process or leave zombie tasks running. Task.Supervisor provides supervised tasks with automatic cleanup and proper error isolation.
# ❌ BARE TASK.ASYNC - Crashes propagate, no cleanup
defmodule DataFetcher do
def fetch_all_data do
tasks = [
Task.async(fn -> fetch_users() end),
Task.async(fn -> fetch_posts() end),
Task.async(fn -> fetch_comments() end)
]
# If fetch_posts() crashes, the entire process crashes!
# Orphaned tasks may continue running in the background
results = Task.await_many(tasks, 5_000)
{:ok, results}
end
end
# ✅ TASK.SUPERVISOR - Isolated tasks with automatic cleanup
defmodule DataFetcher do
def fetch_all_data do
tasks = [
Task.Supervisor.async(MyApp.TaskSupervisor, fn -> fetch_users() end),
Task.Supervisor.async(MyApp.TaskSupervisor, fn -> fetch_posts() end),
Task.Supervisor.async(MyApp.TaskSupervisor, fn -> fetch_comments() end)
]
# Crashes are isolated - other tasks continue
# Failed tasks return {:exit, reason}
results = Task.yield_many(tasks, 5_000)
{:ok, results}
end
end
The key difference: supervised tasks don’t crash their caller and are automatically cleaned up when they complete or fail.
Handling task failures gracefully
Task.yield_many/2 returns results without raising, letting you handle failures individually:
defmodule ReportGenerator do
def generate_reports(user_ids) do
tasks =
Enum.map(user_ids, fn user_id ->
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
generate_user_report(user_id)
end)
end)
tasks
|> Task.yield_many(30_000)
|> Enum.map(fn {task, result} ->
case result do
{:ok, report} ->
report
{:exit, reason} ->
Logger.error("Task failed: #{inspect(reason)}")
nil
nil ->
# Task timed out - kill it
Task.shutdown(task, :brutal_kill)
Logger.error("Task timed out")
nil
end
end)
|> Enum.reject(&is_nil/1)
end
end
Starting a Task.Supervisor in your application
Add the supervisor to your application’s supervision tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Task.Supervisor, name: MyApp.TaskSupervisor}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Now you can use MyApp.TaskSupervisor throughout your application.
Parallel API calls with error handling
When calling multiple external services, supervised tasks prevent one failure from killing everything:
defmodule WeatherAggregator do
def fetch_weather(location) do
apis = [
{"OpenWeather", &OpenWeatherAPI.fetch/1},
{"WeatherAPI", &WeatherAPI.fetch/1},
{"AccuWeather", &AccuWeatherAPI.fetch/1}
]
tasks =
Enum.map(apis, fn {name, fetch_fn} ->
task = Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
fetch_fn.(location)
end)
{name, task}
end)
Enum.map(tasks, fn {name, task} ->
case Task.yield(task, 5_000) || Task.shutdown(task) do
{:ok, result} ->
{name, {:ok, result}}
{:exit, reason} ->
Logger.warn("#{name} API failed: #{inspect(reason)}")
{name, {:error, :api_failed}}
nil ->
Logger.warn("#{name} API timed out")
{name, {:error, :timeout}}
end
end)
end
end
Fire-and-forget tasks with supervision
For background work that shouldn’t block, use Task.Supervisor.start_child/2:
defmodule Analytics do
def track_event(event_data) do
# Don't wait for completion, don't care about result
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
send_to_analytics_service(event_data)
end)
:ok
end
def send_welcome_emails(user_ids) do
Enum.each(user_ids, fn user_id ->
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
user = Repo.get!(User, user_id)
Mailer.send_welcome_email(user)
end)
end)
:ok
end
end
These tasks run independently and clean up automatically when done. If they crash, they don’t affect your process.
Task pools for rate limiting
Limit concurrent tasks by setting max_children on your supervisor:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Task.Supervisor, name: MyApp.TaskSupervisor, max_children: 10}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
When you hit the limit, new tasks wait until a slot opens up, preventing resource exhaustion.
When to use Task.Supervisor
Always use for:
- Parallel API calls or external service calls
- Background work that can fail independently
- CPU-intensive computations that might crash
- Any task where you need graceful error handling
- Fire-and-forget operations that shouldn’t block
Bare Task.async is only safe for:
- Short, guaranteed-safe operations
- Code you control completely with no failure cases
- Truly nothing (use Task.Supervisor everywhere)
Use Task.Supervisor for all concurrent work in production applications. Your processes won’t crash from task failures, and cleanup happens automatically.
copied to clipboard