DEV Community

Cover image for From Promise.all() to asyncio.gather(): The Complete Guide to JavaScript-Style Async Patterns in Python
Akash Thakur
Akash Thakur

Posted on

From Promise.all() to asyncio.gather(): The Complete Guide to JavaScript-Style Async Patterns in Python

If you're a JavaScript developer diving into Python, you've probably wondered: "Where are the Promises?" Python's approach to asynchronous programming is different, but just as powerful. This comprehensive guide will show you how to translate every JavaScript Promise pattern into Python's asyncio equivalents.

The Fundamental Difference

JavaScript uses Promises as objects representing future values, while Python uses coroutines with the async/await syntax directly. Here's the basic comparison:

JavaScript Promise

function fetchData() { return new Promise((resolve) => { setTimeout(() => resolve("Data fetched!"), 1000); }); } fetchData().then(result => console.log(result)); 
Enter fullscreen mode Exit fullscreen mode

Python Coroutine

import asyncio async def fetch_data(): await asyncio.sleep(1) return "Data fetched!" async def main(): result = await fetch_data() print(result) asyncio.run(main()) 
Enter fullscreen mode Exit fullscreen mode

Promise.all() → asyncio.gather()

Use case: Wait for multiple asynchronous operations to complete simultaneously.

JavaScript Promise.all()

async function fetchMultipleData() { const urls = [ 'https://api.example.com/users', 'https://api.example.com/posts', 'https://api.example.com/comments' ]; const promises = urls.map(url => fetch(url).then(r => r.json())); try { const results = await Promise.all(promises); console.log('All data fetched:', results); return results; } catch (error) { console.error('One or more requests failed:', error); throw error; } } 
Enter fullscreen mode Exit fullscreen mode

Python asyncio.gather()

import asyncio import aiohttp async def fetch_data(session, url): async with session.get(url) as response: return await response.json() async def fetch_multiple_data(): urls = [ 'https://api.example.com/users', 'https://api.example.com/posts', 'https://api.example.com/comments' ] async with aiohttp.ClientSession() as session: try: results = await asyncio.gather( *[fetch_data(session, url) for url in urls] ) print('All data fetched:', results) return results except Exception as error: print('One or more requests failed:', error) raise error # Alternative with error handling per task async def fetch_multiple_data_safe(): urls = [ 'https://api.example.com/users', 'https://api.example.com/posts', 'https://api.example.com/comments' ] async with aiohttp.ClientSession() as session: results = await asyncio.gather( *[fetch_data(session, url) for url in urls], return_exceptions=True # Don't fail on first exception  ) # Process results and exceptions  for i, result in enumerate(results): if isinstance(result, Exception): print(f'Request {i} failed: {result}') else: print(f'Request {i} succeeded: {len(result)} items') return results 
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript: Promise.all([...promises])
  • Python: asyncio.gather(*coroutines) (note the unpacking with *)
  • Error Handling: Python's return_exceptions=True lets you handle individual failures

Promise.race() → asyncio.wait() with FIRST_COMPLETED

Use case: Return the result of whichever operation completes first.

JavaScript Promise.race()

async function raceExample() { const slowPromise = new Promise(resolve => setTimeout(() => resolve('Slow result'), 3000) ); const fastPromise = new Promise(resolve => setTimeout(() => resolve('Fast result'), 1000) ); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000) ); try { const result = await Promise.race([slowPromise, fastPromise, timeoutPromise]); console.log('First result:', result); // "Fast result" return result; } catch (error) { console.error('First to complete was an error:', error); throw error; } } 
Enter fullscreen mode Exit fullscreen mode

Python asyncio.wait() with FIRST_COMPLETED

async def slow_task(): await asyncio.sleep(3) return 'Slow result' async def fast_task(): await asyncio.sleep(1) return 'Fast result' async def timeout_task(): await asyncio.sleep(2) raise Exception('Timeout') async def race_example(): tasks = [ asyncio.create_task(slow_task()), asyncio.create_task(fast_task()), asyncio.create_task(timeout_task()) ] try: done, pending = await asyncio.wait( tasks, return_when=asyncio.FIRST_COMPLETED ) # Get the first completed result  first_task = list(done)[0] result = first_task.result() # This will raise if the task failed  print('First result:', result) # "Fast result"  # Cancel remaining tasks  for task in pending: task.cancel() return result except Exception as error: print('First to complete was an error:', error) # Cancel all remaining tasks  for task in tasks: if not task.done(): task.cancel() raise error # Alternative: Using asyncio.wait_for() for timeout patterns async def race_with_timeout(): try: result = await asyncio.wait_for(slow_task(), timeout=2.0) return result except asyncio.TimeoutError: print("Operation timed out") raise 
Enter fullscreen mode Exit fullscreen mode

Promise.allSettled() → asyncio.wait() with ALL_COMPLETED

Use case: Wait for all operations to complete, regardless of success or failure.

JavaScript Promise.allSettled()

async function allSettledExample() { const promises = [ Promise.resolve('Success 1'), Promise.reject(new Error('Error 1')), Promise.resolve('Success 2'), new Promise(resolve => setTimeout(() => resolve('Delayed success'), 1000)) ]; const results = await Promise.allSettled(promises); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Promise ${index} succeeded:`, result.value); } else { console.log(`Promise ${index} failed:`, result.reason); } }); return results; } 
Enter fullscreen mode Exit fullscreen mode

Python asyncio.wait() with ALL_COMPLETED

async def success_task_1(): return 'Success 1' async def error_task(): raise Exception('Error 1') async def success_task_2(): return 'Success 2' async def delayed_task(): await asyncio.sleep(1) return 'Delayed success' async def all_settled_example(): tasks = [ asyncio.create_task(success_task_1()), asyncio.create_task(error_task()), asyncio.create_task(success_task_2()), asyncio.create_task(delayed_task()) ] done, pending = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) results = [] for i, task in enumerate(tasks): try: result = task.result() print(f'Task {i} succeeded: {result}') results.append({'status': 'fulfilled', 'value': result}) except Exception as error: print(f'Task {i} failed: {error}') results.append({'status': 'rejected', 'reason': str(error)}) return results # Python 3.11+ TaskGroup approach (Recommended) async def all_settled_with_task_group(): results = [] # Handle successful tasks  async def safe_task(task_func, task_name): try: result = await task_func() return {'status': 'fulfilled', 'value': result, 'name': task_name} except Exception as error: return {'status': 'rejected', 'reason': str(error), 'name': task_name} tasks = [ safe_task(success_task_1, 'task1'), safe_task(error_task, 'task2'), safe_task(success_task_2, 'task3'), safe_task(delayed_task, 'task4') ] results = await asyncio.gather(*tasks) for result in results: if result['status'] == 'fulfilled': print(f"{result['name']} succeeded: {result['value']}") else: print(f"{result['name']} failed: {result['reason']}") return results 
Enter fullscreen mode Exit fullscreen mode

Promise.any() → Custom Implementation

Use case: Return the first successful result, or fail if all operations fail.

JavaScript Promise.any()

async function anyExample() { const promises = [ Promise.reject(new Error('Error 1')), Promise.reject(new Error('Error 2')), new Promise(resolve => setTimeout(() => resolve('Success!'), 1000)), Promise.reject(new Error('Error 3')) ]; try { const result = await Promise.any(promises); console.log('First success:', result); // "Success!" return result; } catch (aggregateError) { console.error('All promises failed:', aggregateError.errors); throw aggregateError; } } 
Enter fullscreen mode Exit fullscreen mode

Python Custom Implementation

class AllFailedError(Exception): def __init__(self, errors): self.errors = errors super().__init__(f"All operations failed: {errors}") async def any_successful(*coroutines): """Python equivalent of Promise.any()""" if not coroutines: raise ValueError("At least one coroutine required") tasks = [asyncio.create_task(coro) for coro in coroutines] exceptions = [] try: while tasks: done, tasks = await asyncio.wait( tasks, return_when=asyncio.FIRST_COMPLETED ) for task in done: try: result = task.result() # Cancel remaining tasks  for remaining_task in tasks: remaining_task.cancel() return result except Exception as error: exceptions.append(error) # All tasks failed  raise AllFailedError(exceptions) finally: # Ensure all tasks are cancelled  for task in tasks: if not task.done(): task.cancel() # Usage example async def failing_task_1(): await asyncio.sleep(0.5) raise Exception('Error 1') async def failing_task_2(): await asyncio.sleep(0.3) raise Exception('Error 2') async def success_task(): await asyncio.sleep(1) return 'Success!' async def failing_task_3(): await asyncio.sleep(0.8) raise Exception('Error 3') async def any_example(): try: result = await any_successful( failing_task_1(), failing_task_2(), success_task(), failing_task_3() ) print('First success:', result) # "Success!"  return result except AllFailedError as error: print('All operations failed:', error.errors) raise error 
Enter fullscreen mode Exit fullscreen mode

Modern Python: TaskGroup (Python 3.11+)

Python 3.11 introduced TaskGroup, which provides a more structured way to handle concurrent tasks:

# TaskGroup automatically handles cleanup and error propagation async def modern_concurrent_example(): async with asyncio.TaskGroup() as tg: task1 = tg.create_task(fetch_data('https://api.example.com/users')) task2 = tg.create_task(fetch_data('https://api.example.com/posts')) task3 = tg.create_task(fetch_data('https://api.example.com/comments')) # All tasks completed successfully if we reach here  return [task1.result(), task2.result(), task3.result()] # TaskGroup with exception handling async def modern_with_error_handling(): try: async with asyncio.TaskGroup() as tg: task1 = tg.create_task(success_task_1()) task2 = tg.create_task(error_task()) # This will cause TaskGroup to fail  task3 = tg.create_task(success_task_2()) # Won't reach here if any task fails  return [task1.result(), task2.result(), task3.result()] except* Exception as eg: # Exception groups (Python 3.11+)  for error in eg.exceptions: print(f"Task failed: {error}") raise 
Enter fullscreen mode Exit fullscreen mode

Performance Comparison

Here's a practical example showing the performance benefits of concurrent execution:

import time import asyncio import aiohttp async def time_comparison(): urls = [ 'https://httpbin.org/delay/1', 'https://httpbin.org/delay/1', 'https://httpbin.org/delay/1' ] # Sequential execution  start_time = time.time() async with aiohttp.ClientSession() as session: results_sequential = [] for url in urls: async with session.get(url) as response: results_sequential.append(await response.json()) sequential_time = time.time() - start_time # Concurrent execution with asyncio.gather()  start_time = time.time() async with aiohttp.ClientSession() as session: async def fetch(url): async with session.get(url) as response: return await response.json() results_concurrent = await asyncio.gather(*[fetch(url) for url in urls]) concurrent_time = time.time() - start_time print(f"Sequential execution: {sequential_time:.2f} seconds") print(f"Concurrent execution: {concurrent_time:.2f} seconds") print(f"Speedup: {sequential_time/concurrent_time:.2f}x") # Expected output: # Sequential execution: 3.15 seconds # Concurrent execution: 1.12 seconds # Speedup: 2.81x 
Enter fullscreen mode Exit fullscreen mode

Quick Reference Cheat Sheet

JavaScript Python Use Case
Promise.all(promises) asyncio.gather(*coroutines) Wait for all, fail fast
Promise.allSettled(promises) asyncio.wait(tasks, return_when=ALL_COMPLETED) Wait for all, collect results/errors
Promise.race(promises) asyncio.wait(tasks, return_when=FIRST_COMPLETED) Return first to complete
Promise.any(promises) Custom any_successful() function Return first success
new Promise((resolve, reject) => ...) async def coroutine(): ... Create async operation
promise.then().catch() try: await coroutine() except: Handle results/errors
Promise.resolve(value) return value (in async function) Return immediate value
Promise.reject(error) raise error (in async function) Return immediate error

Best Practices

  1. Use asyncio.gather() for the most common concurrent patterns - it's the closest equivalent to Promise.all()

  2. Prefer TaskGroup (Python 3.11+) for better error handling and resource cleanup

  3. Use return_exceptions=True with gather() when you want to handle individual failures

  4. Always cancel pending tasks when using asyncio.wait() to prevent resource leaks

  5. Use aiohttp instead of requests for HTTP operations in async code

  6. Consider asyncio.wait_for() for simple timeout scenarios instead of implementing race conditions

Conclusion

While Python doesn't have Promises, its asyncio patterns are equally powerful and often more explicit about error handling and resource management. The key is understanding that Python's async/await syntax works directly with coroutines, eliminating the need for Promise wrapper objects.

Whether you're migrating from JavaScript or just learning async patterns, these equivalents will help you write efficient, concurrent Python code that scales beautifully.

Top comments (0)