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 Promiseawait()
- Waits for a Promise to resolve and returns its valueall()
- Runs multiple Promises concurrently and waits for all to completerace()
- Runs multiple Promises concurrently and returns the first to completeany()
- Returns the first Promise to successfully resolvemap()
- Processes an array of items with controlled concurrencybatch()
- Processes items in batches with controlled concurrencyretry()
- Retries an async operation with exponential backoff
Making Asynchronous Requests
To make asynchronous requests, wrap your fetch()
calls with the async()
function:
// 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:
// 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:
// 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:
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:
// 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:
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):
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:
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:
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:
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:
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:
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:
$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
Fetching Related Resources
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
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
Use Async for Multiple Requests: Asynchronous requests are most beneficial when making multiple independent HTTP requests.
Control Concurrency: Don't create too many concurrent requests. Use
map()
orbatch()
with a reasonable concurrency limit.Handle All Errors: Always include error handling with try/catch or
.catch()
for async operations.Keep Functions Pure: Avoid side effects in async functions for better predictability.
Combine with Retries: Use the
retry()
function for more resilient async operations.Set Appropriate Timeouts: Use
timeout()
to prevent operations from hanging indefinitely.Avoid Mixing Sync and Async: When using async, make all your HTTP operations async for consistent code patterns.
Be Mindful of Server Load: While async lets you make many requests in parallel, be mindful of rate limits and server capacity.
Next Steps
- Learn about Promise Operations for more advanced async patterns
- Explore Error Handling for handling errors in async code
- See Retry Handling for making async requests more resilient