# Use `Task.Supervisor` instead of bare `Task.async`

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.

```elixir
# ❌ 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:

```elixir
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:

```elixir
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:

```elixir
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`:

```elixir
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:

```elixir
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.


---

Created by: almirsarajcic
Date: November 19, 2025
URL: https://elixirdrops.net/d/sxEpwcGD
