Skip to content

asyncio.gather() vs asyncio.wait()

Both functions run multiple coroutines concurrently, but with different behaviors and use cases.

Quick Comparison

Feature gather() wait()
Input Coroutines or tasks Tasks only (must wrap coroutines)
Returns List of results Two sets: (done, pending)
Result order Same as input order Completion order
Exception handling Configurable Manual checking
Partial completion No Yes (FIRST_COMPLETED, timeout)
Cancellation Group cancellation Individual task control

asyncio.gather()

Basic Usage

import asyncio

async def fetch(url, delay):
    await asyncio.sleep(delay)
    return f"Data from {url}"

async def main():
    # Pass coroutines directly
    results = await asyncio.gather(
        fetch("url1", 2),
        fetch("url2", 1),
        fetch("url3", 3)
    )

    # Results in input order (not completion order)
    print(results)
    # ['Data from url1', 'Data from url2', 'Data from url3']

asyncio.run(main())

Exception Handling

async def might_fail(n):
    if n == 2:
        raise ValueError(f"Error on {n}")
    return n * 10

async def main():
    # Default: First exception propagates, others cancelled
    try:
        results = await asyncio.gather(
            might_fail(1),
            might_fail(2),  # Raises
            might_fail(3)
        )
    except ValueError as e:
        print(f"Caught: {e}")

    # With return_exceptions=True: Exceptions returned in results
    results = await asyncio.gather(
        might_fail(1),
        might_fail(2),
        might_fail(3),
        return_exceptions=True
    )
    print(results)
    # [10, ValueError('Error on 2'), 30]

    # Check for exceptions
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"Task {i} failed: {result}")
        else:
            print(f"Task {i} succeeded: {result}")

asyncio.run(main())

When to Use gather()

  • You need all results in input order
  • Simple concurrent execution
  • Want to cancel all if one fails (default behavior)
  • Want exceptions as values (return_exceptions=True)

asyncio.wait()

Basic Usage

import asyncio

async def fetch(url, delay):
    await asyncio.sleep(delay)
    return f"Data from {url}"

async def main():
    # Must pass tasks (not coroutines)
    tasks = [
        asyncio.create_task(fetch("url1", 2)),
        asyncio.create_task(fetch("url2", 1)),
        asyncio.create_task(fetch("url3", 3))
    ]

    # Returns two sets
    done, pending = await asyncio.wait(tasks)

    # Process completed tasks
    for task in done:
        result = task.result()
        print(result)

asyncio.run(main())

Return When Options

async def main():
    tasks = [
        asyncio.create_task(fetch("url1", 2)),
        asyncio.create_task(fetch("url2", 1)),
        asyncio.create_task(fetch("url3", 3))
    ]

    # FIRST_COMPLETED: Return when any task completes
    done, pending = await asyncio.wait(
        tasks,
        return_when=asyncio.FIRST_COMPLETED
    )
    print(f"First done: {len(done)}, pending: {len(pending)}")

    # Continue with remaining
    done, pending = await asyncio.wait(pending)
async def main():
    tasks = [
        asyncio.create_task(might_fail(1)),
        asyncio.create_task(might_fail(2)),  # Raises
        asyncio.create_task(might_fail(3))
    ]

    # FIRST_EXCEPTION: Return when first exception or all done
    done, pending = await asyncio.wait(
        tasks,
        return_when=asyncio.FIRST_EXCEPTION
    )

    # Check which failed
    for task in done:
        if task.exception():
            print(f"Failed: {task.exception()}")
        else:
            print(f"Succeeded: {task.result()}")

    # Cancel remaining
    for task in pending:
        task.cancel()

Timeout

async def main():
    tasks = [
        asyncio.create_task(fetch("fast", 1)),
        asyncio.create_task(fetch("slow", 10))
    ]

    # Wait at most 2 seconds
    done, pending = await asyncio.wait(tasks, timeout=2.0)

    print(f"Completed: {len(done)}")
    print(f"Still pending: {len(pending)}")

    # Handle pending tasks
    for task in pending:
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass

asyncio.run(main())

Exception Handling with wait()

async def main():
    tasks = [
        asyncio.create_task(might_fail(1)),
        asyncio.create_task(might_fail(2)),
        asyncio.create_task(might_fail(3))
    ]

    done, pending = await asyncio.wait(tasks)

    results = []
    errors = []

    for task in done:
        try:
            results.append(task.result())
        except Exception as e:
            errors.append(e)

    print(f"Results: {results}")
    print(f"Errors: {errors}")

When to Use wait()

  • Need partial results (FIRST_COMPLETED)
  • Want to handle timeouts with pending task control
  • Need to detect first exception without cancelling others
  • Want fine-grained control over individual tasks
  • Need to process results as they complete

Side-by-Side Examples

Example 1: Fetch All URLs

# Using gather() - simpler
async def fetch_all_gather(urls):
    async with aiohttp.ClientSession() as session:
        results = await asyncio.gather(*[
            fetch_url(session, url) for url in urls
        ])
    return results  # In input order

# Using wait() - more verbose
async def fetch_all_wait(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [
            asyncio.create_task(fetch_url(session, url))
            for url in urls
        ]
        done, _ = await asyncio.wait(tasks)

    return [task.result() for task in done]  # Completion order

Example 2: First Response Wins

# wait() is better here
async def first_response(urls):
    tasks = [
        asyncio.create_task(fetch_url(url), name=url)
        for url in urls
    ]

    done, pending = await asyncio.wait(
        tasks,
        return_when=asyncio.FIRST_COMPLETED
    )

    # Cancel others
    for task in pending:
        task.cancel()

    # Get first result
    first_task = done.pop()
    return first_task.result()

Example 3: Timeout with Partial Results

# wait() with timeout
async def fetch_with_deadline(urls, timeout=5.0):
    tasks = [
        asyncio.create_task(fetch_url(url))
        for url in urls
    ]

    done, pending = await asyncio.wait(tasks, timeout=timeout)

    # Collect completed results
    results = []
    for task in done:
        try:
            results.append(task.result())
        except Exception:
            pass

    # Cancel slow tasks
    for task in pending:
        task.cancel()

    return results

# gather() can't do partial results with timeout

Example 4: Aggregate Errors

# gather() with return_exceptions
async def fetch_collecting_errors_gather(urls):
    results = await asyncio.gather(
        *[fetch_url(url) for url in urls],
        return_exceptions=True
    )

    successes = [r for r in results if not isinstance(r, Exception)]
    failures = [r for r in results if isinstance(r, Exception)]

    return successes, failures

# wait() equivalent
async def fetch_collecting_errors_wait(urls):
    tasks = [asyncio.create_task(fetch_url(url)) for url in urls]
    done, _ = await asyncio.wait(tasks)

    successes = []
    failures = []

    for task in done:
        try:
            successes.append(task.result())
        except Exception as e:
            failures.append(e)

    return successes, failures

asyncio.as_completed()

For processing results in completion order:

async def process_as_completed(urls):
    tasks = [asyncio.create_task(fetch_url(url)) for url in urls]

    for coro in asyncio.as_completed(tasks):
        try:
            result = await coro
            print(f"Got: {result}")
            # Process immediately, don't wait for all
        except Exception as e:
            print(f"Error: {e}")

Decision Guide

Need results in input order?
├─ Yes → gather()
└─ No
   ├─ Need timeout with partial results? → wait(timeout=...)
   ├─ Need first completed? → wait(return_when=FIRST_COMPLETED)
   ├─ Need to detect first exception? → wait(return_when=FIRST_EXCEPTION)
   └─ Process as completed? → as_completed()

Key Takeaways

  • gather(): Simple, ordered results, group error handling
  • wait(): Flexible, partial completion, timeout support
  • as_completed(): Process results in completion order
  • gather() takes coroutines directly; wait() requires tasks
  • gather() returns results list; wait() returns (done, pending) sets
  • Use gather() for most cases; use wait() when you need control

Runnable Example: gather_vs_wait_tutorial.py

"""
Intermediate Tutorial 4: gather() vs wait()

Compares asyncio.gather() and asyncio.wait() for different use cases.
"""

import asyncio

# =============================================================================
# Definitions
# =============================================================================

async def demonstrate_gather_vs_wait():
    print("\n--- gather() vs wait() ---")
    print("""
    gather():
    • Returns results in order
    • Simple API
    • Best for: Getting all results

    wait():
    • Returns (done, pending) sets
    • More control
    • Best for: Processing as completed, timeouts

    Example - gather():
    results = await asyncio.gather(task1(), task2(), task3())

    Example - wait():
    done, pending = await asyncio.wait([task1(), task2()])
    for task in done:
        result = await task

    Use gather() for most cases!
    """)

async def main():
    await demonstrate_gather_vs_wait()

# =============================================================================
# Main
# =============================================================================

if __name__ == "__main__":
    asyncio.run(main())