Skip to content

Asynchronous Requests

This guide explains how to work with asynchronous HTTP requests in the Fetch HTTP package. Asynchronous requests allow you to execute multiple HTTP operations in parallel, which can significantly improve performance when making multiple independent requests.

Understanding Async/Await in PHP

Fetch PHP brings JavaScript-like async/await patterns to PHP through the Matrix library, which is integrated into the package. While PHP doesn't have native language support for async/await, the package provides functions that enable similar functionality.

The key functions for async operations are:

  • async() - Wraps a function to run asynchronously, returning a Promise
  • await() - Waits for a Promise to resolve and returns its value
  • all() - Runs multiple Promises concurrently and waits for all to complete
  • race() - Runs multiple Promises concurrently and returns the first to complete
  • any() - Returns the first Promise to successfully resolve
  • map() - Processes an array of items with controlled concurrency
  • batch() - Processes items in batches with controlled concurrency
  • retry() - Retries an async operation with exponential backoff

Making Asynchronous Requests

To make asynchronous requests, wrap your fetch() calls with the async() function:

php
// Import the async functions
use function async;
use function await;
use function all;

// Create a promise for an async request
$promise = async(function() {
    return fetch('https://api.example.com/users');
});

// Wait for the promise to resolve
$response = await($promise);
$users = $response->json();

Multiple Concurrent Requests

One of the main benefits of async requests is the ability to execute multiple HTTP requests in parallel:

php
// Execute an async function
await(async(function() {
    // Create multiple requests
    $results = await(all([
        'users' => async(fn() => fetch('https://api.example.com/users')),
        'posts' => async(fn() => fetch('https://api.example.com/posts')),
        'comments' => async(fn() => fetch('https://api.example.com/comments'))
    ]));

    // Process the results
    $users = $results['users']->json();
    $posts = $results['posts']->json();
    $comments = $results['comments']->json();

    echo "Fetched " . count($users) . " users, " .
         count($posts) . " posts, and " .
         count($comments) . " comments";
}));

Traditional Promise-based Pattern

In addition to the async/await pattern, you can use the more traditional promise-based approach:

php
// Get the handler for async operations
$handler = fetch_client()->getHandler();
$handler->async();

// Make the async request
$promise = $handler->get('https://api.example.com/users');

// Handle the result with callbacks
$promise->then(
    function ($response) {
        // Process successful response
        $users = $response->json();
        foreach ($users as $user) {
            echo $user['name'] . PHP_EOL;
        }
    },
    function ($exception) {
        // Handle errors
        echo "Error: " . $exception->getMessage();
    }
);

Promise Chaining

You can chain operations to be executed when a promise resolves:

php
async(function() {
    return fetch('https://api.example.com/users');
})
->then(function($response) {
    // This runs when the request succeeds
    $users = $response->json();
    echo "Fetched " . count($users) . " users";
    return $users;
})
->then(function($users) {
    // Process the users
    return array_map(function($user) {
        return $user['name'];
    }, $users);
})
->then(function($userNames) {
    echo "User names: " . implode(', ', $userNames);
});

Error Handling

Handle errors in asynchronous code with try/catch in an async function or the catch() method:

php
// Using try/catch with await
await(async(function() {
    try {
        $response = await(async(function() {
            return fetch('https://api.example.com/users/999');
        }));

        if ($response->isNotFound()) {
            throw new \Exception("User not found");
        }

        return $response->json();
    } catch (\Exception $e) {
        echo "Error: " . $e->getMessage();
        return [];
    }
}));

// Using catch() with promises
$handler = fetch_client()->getHandler();
$handler->async()
    ->get('https://api.example.com/users/999')
    ->then(function($response) {
        if ($response->failed()) {
            throw new \Exception("API returned error: " . $response->status());
        }
        return $response->json();
    })
    ->catch(function($error) {
        echo "Error: " . $error->getMessage();
        return [];
    });

Using race() to Get the First Result

Sometimes you may want whichever request finishes first:

php
use function race;

// Create promises for redundant endpoints
$promises = [
    async(fn() => fetch('https://api1.example.com/data')),
    async(fn() => fetch('https://api2.example.com/data')),
    async(fn() => fetch('https://api3.example.com/data'))
];

// Get the result from whichever completes first
$response = await(race($promises));
$data = $response->json();
echo "Got data from the fastest source";

Using any() to Get the First Success

To get the first successful result (ignoring failures):

php
use function any;

// Create promises for redundant endpoints
$promises = [
    async(fn() => fetch('https://api1.example.com/data')),
    async(fn() => fetch('https://api2.example.com/data')),
    async(fn() => fetch('https://api3.example.com/data'))
];

// Get the first successful result
try {
    $response = await(any($promises));
    $data = $response->json();
    echo "Got data from the first successful source";
} catch (\Exception $e) {
    echo "All requests failed";
}

Controlled Concurrency with map()

For processing many items with controlled parallelism:

php
use function map;

// List of user IDs to fetch
$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Process at most 3 requests at a time
$responses = await(map($userIds, function($id) {
    return async(function() use ($id) {
        return fetch("https://api.example.com/users/{$id}");
    });
}, 3));

// Process the responses
foreach ($responses as $index => $response) {
    $user = $response->json();
    echo "Processed user {$user['name']}\n";
}

Batch Processing

For processing items in batches with controlled concurrency:

php
use function batch;

// Array of items to process
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Process in batches of 3 with max 2 concurrent batches
$results = await(batch(
    $items,
    function($batch) {
        // Process a batch
        return async(function() use ($batch) {
            $batchResults = [];
            foreach ($batch as $id) {
                $response = await(async(fn() =>
                    fetch("https://api.example.com/users/{$id}")
                ));
                $batchResults[] = $response->json();
            }
            return $batchResults;
        });
    },
    3, // batch size
    2  // concurrency
));

Sequential Async Requests

Sometimes you need to execute requests in sequence, where each depends on the previous:

php
await(async(function() {
    // First request: get auth token
    $authResponse = await(async(fn() =>
        fetch('https://api.example.com/auth/login', [
            'method' => 'POST',
            'json' => [
                'username' => 'user',
                'password' => 'pass'
            ]
        ])
    ));

    $token = $authResponse->json()['token'];

    // Second request: use token to get user profile
    $profileResponse = await(async(fn() =>
        fetch('https://api.example.com/me', [
            'token' => $token
        ])
    ));

    $user = $profileResponse->json();

    // Third request: get user's posts
    $postsResponse = await(async(fn() =>
        fetch("https://api.example.com/users/{$user['id']}/posts", [
            'token' => $token
        ])
    ));

    return $postsResponse->json();
}));

Retries with Async

You can combine async operations with retry logic for more resilient requests:

php
use function retry;

// Retry a flaky request up to 3 times with exponential backoff
$data = await(retry(
    function() {
        return async(function() {
            return fetch('https://api.example.com/unstable-endpoint');
        });
    },
    3, // max attempts
    function($attempt) {
        // Exponential backoff strategy
        return min(pow(2, $attempt) * 100, 1000);
    }
));

Timeouts with async/await

You can set a timeout when waiting for a promise:

php
use function timeout;

try {
    // Add a 5-second timeout to a request
    $response = await(timeout(
        async(fn() => fetch('https://api.example.com/slow-endpoint')),
        5.0
    ));

    $data = $response->json();
} catch (\Matrix\Exceptions\TimeoutException $e) {
    echo "Timeout error: " . $e->getMessage();
}

Using the Handler's Promise Utilities

The ClientHandler class provides methods for working with promises:

php
$handler = fetch_client()->getHandler();

// Execute multiple promises concurrently
$promises = [
    async(fn() => fetch('https://api.example.com/users')),
    async(fn() => fetch('https://api.example.com/posts'))
];

// Using the handler's promise utilities
$results = $handler->awaitPromise($handler->all($promises));

// The handler can also create resolved or rejected promises
$resolved = $handler->resolve(['name' => 'John']);
$rejected = $handler->reject(new \Exception('Something went wrong'));

Real-World Examples

php
await(async(function() {
    // Get a user and their related data in parallel
    $userId = 123;

    // First, get the user
    $userResponse = await(async(function() use ($userId) {
        return fetch("https://api.example.com/users/{$userId}");
    }));

    $user = $userResponse->json();

    // Then fetch posts and followers in parallel
    $related = await(all([
        'posts' => async(fn() => fetch("https://api.example.com/users/{$user['id']}/posts")),
        'followers' => async(fn() => fetch("https://api.example.com/users/{$user['id']}/followers"))
    ]));

    $data = [
        'user' => $user,
        'posts' => $related['posts']->json(),
        'followers' => $related['followers']->json()
    ];

    echo "User: {$data['user']['name']}\n";
    echo "Posts: " . count($data['posts']) . "\n";
    echo "Followers: " . count($data['followers']) . "\n";

    return $data;
}));

Implementing Pagination with Async

php
await(async(function() {
    $url = 'https://api.example.com/users';
    $perPage = 100;
    $allItems = [];

    // Get the first page
    $firstPageResponse = await(async(function() use ($url, $perPage) {
        return fetch("{$url}?per_page={$perPage}&page=1");
    }));

    $firstPageData = $firstPageResponse->json();
    $items = $firstPageData['items'] ?? $firstPageData;
    $allItems = array_merge($allItems, $items);

    $totalCount = $firstPageResponse->header('X-Total-Count')
        ? (int)$firstPageResponse->header('X-Total-Count')
        : count($items);
    $totalPages = ceil($totalCount / $perPage);

    // If we have multiple pages, fetch them all in parallel
    if ($totalPages > 1) {
        $pagePromises = [];

        for ($page = 2; $page <= $totalPages; $page++) {
            $pagePromises[] = async(function() use ($url, $perPage, $page) {
                return fetch("{$url}?per_page={$perPage}&page={$page}");
            });
        }

        // Wait for all pages
        $pageResponses = await(all($pagePromises));

        // Process all responses
        foreach ($pageResponses as $response) {
            $pageData = $response->json();
            $pageItems = $pageData['items'] ?? $pageData;
            $allItems = array_merge($allItems, $pageItems);
        }
    }

    return $allItems;
}));

Best Practices for Async Requests

  1. Use Async for Multiple Requests: Asynchronous requests are most beneficial when making multiple independent HTTP requests.

  2. Control Concurrency: Don't create too many concurrent requests. Use map() or batch() with a reasonable concurrency limit.

  3. Handle All Errors: Always include error handling with try/catch or .catch() for async operations.

  4. Keep Functions Pure: Avoid side effects in async functions for better predictability.

  5. Combine with Retries: Use the retry() function for more resilient async operations.

  6. Set Appropriate Timeouts: Use timeout() to prevent operations from hanging indefinitely.

  7. Avoid Mixing Sync and Async: When using async, make all your HTTP operations async for consistent code patterns.

  8. Be Mindful of Server Load: While async lets you make many requests in parallel, be mindful of rate limits and server capacity.

Next Steps

Released under the MIT License. A modern HTTP client for PHP developers.