Promise Operations
This guide covers how to work with promises in the Fetch HTTP package. The package implements a Promise-based API similar to JavaScript promises, allowing for sophisticated asynchronous programming patterns.
Basic Promise Concepts
Promises represent values that may not be available yet. They're used for asynchronous operations like HTTP requests. In the Fetch HTTP package, promises are represented by the PromiseInterface
from React's Promise library.
Creating Promises
There are several ways to create promises:
// Create a promise from an async function
$promise = async(function() {
return fetch('https://api.example.com/users');
});
// Create a resolved promise with a value
$resolvedPromise = resolve(['name' => 'John', 'email' => 'john@example.com']);
// Through the ClientHandler
$handler = fetch_client()->getHandler();
$resolvedPromise = $handler->resolve(['name' => 'John']);
// Create a rejected promise with an error
$rejectedPromise = reject(new \Exception('Something went wrong'));
// Through the ClientHandler
$handler = fetch_client()->getHandler();
$rejectedPromise = $handler->reject(new \Exception('Something went wrong'));
Promise Methods
then()
The then()
method registers callbacks for when a promise resolves successfully or fails:
$promise = async(function() {
return fetch('https://api.example.com/users');
});
$promise->then(
function ($response) {
// Success callback
$users = $response->json();
echo "Fetched " . count($users) . " users";
return $users;
},
function ($error) {
// Error callback
echo "Error: " . $error->getMessage();
}
);
The then()
method returns a new promise that resolves with the return value of the callback.
catch()
The catch()
method is a shorthand for handling errors:
$promise = async(function() {
return fetch('https://api.example.com/users');
});
$promise
->then(function ($response) {
$users = $response->json();
echo "Fetched " . count($users) . " users";
return $users;
})
->catch(function ($error) {
echo "Error: " . $error->getMessage();
});
finally()
The finally()
method registers a callback that runs when the promise settles, regardless of whether it was resolved or rejected:
$promise = async(function() {
return fetch('https://api.example.com/users');
});
$promise
->then(function ($response) {
$users = $response->json();
echo "Fetched " . count($users) . " users";
})
->catch(function ($error) {
echo "Error: " . $error->getMessage();
})
->finally(function () {
echo "Request completed.";
});
Using with ClientHandler
The ClientHandler
provides methods for working with promises:
// Get the handler from the client
$handler = fetch_client()->getHandler();
// Enable async mode
$handler->async();
// Make an async request
$promise = $handler->get('https://api.example.com/users');
// Add callbacks
$promise->then(
function ($response) {
$users = $response->json();
return $users;
}
)->catch(
function ($error) {
echo "Error: " . $error->getMessage();
}
);
// Wait for a promise to resolve
$result = $handler->awaitPromise($promise);
Combining Multiple Promises
The package provides several functions for working with multiple promises:
all()
The all()
function waits for all promises to resolve, or rejects if any promise fails:
// Create multiple promises
$usersPromise = async(function() {
return fetch('https://api.example.com/users');
});
$postsPromise = async(function() {
return fetch('https://api.example.com/posts');
});
$commentsPromise = async(function() {
return fetch('https://api.example.com/comments');
});
// Wait for all to complete
all([
'users' => $usersPromise,
'posts' => $postsPromise,
'comments' => $commentsPromise
])->then(function ($results) {
// $results is an array with keys 'users', 'posts', 'comments'
$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo "Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
});
// Using the handler
$handler = fetch_client()->getHandler();
$results = $handler->awaitPromise($handler->all([
'users' => $usersPromise,
'posts' => $postsPromise
]));
If you use numeric keys, the results will be returned in the same order:
all([
$usersPromise,
$postsPromise,
$commentsPromise
])->then(function ($results) {
// $results is an indexed array
$users = $results[0]->json();
$posts = $results[1]->json();
$comments = $results[2]->json();
});
race()
The race()
function waits for the first promise to settle (resolve or reject):
// 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 (success or failure)
race($promises)
->then(function ($response) {
$data = $response->json();
echo "Got data from the fastest source";
});
// Using the handler
$handler = fetch_client()->getHandler();
$result = $handler->awaitPromise($handler->race($promises));
any()
The any()
function waits for the first promise to resolve, ignoring rejections unless all promises reject:
// Create promises with some that might fail
$promises = [
async(fn() => fetch('https://api1.example.com/data')), // Might fail
async(fn() => fetch('https://api2.example.com/data')), // Might fail
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the first successful result
any($promises)
->then(function ($response) {
$data = $response->json();
echo "Got data from the first successful source";
})
->catch(function ($errors) {
echo "All requests failed!";
});
// Using the handler
$handler = fetch_client()->getHandler();
try {
$result = $handler->awaitPromise($handler->any($promises));
} catch (\Exception $e) {
echo "All requests failed!";
}
Using await() with Promise Combinators
You can also use await()
with the promise combinators for a more synchronous-looking code:
await(async(function() {
// Wait for multiple promises with all()
$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts'))
]));
$users = $results['users']->json();
$posts = $results['posts']->json();
echo "Fetched " . count($users) . " users and " . count($posts) . " posts";
}));
Sequential Operations
You can perform sequential asynchronous operations using await()
:
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 data
$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return $userResponse->json();
}));
Controlled Concurrency with map()
The map()
function applies an async function to each item in an array with controlled concurrency:
use function map;
// List of user IDs
$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Fetch user details for each ID, with at most 3 concurrent requests
$responses = await(map($userIds, function ($userId) {
return async(function() use ($userId) {
return fetch("https://api.example.com/users/{$userId}");
});
}, 3));
$users = [];
foreach ($responses as $response) {
$users[] = $response->json();
}
echo "Fetched details for " . count($users) . " users";
You can also use the handler's map method:
$handler = fetch_client()->getHandler();
$responses = $handler->awaitPromise($handler->map($userIds, function($id) use ($handler) {
return $handler->wrapAsync(function() use ($id) {
return fetch("https://api.example.com/users/{$id}");
});
}, 3));
Batch Processing
For processing items in batches rather than one at a time:
use function batch;
// List of user IDs
$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
// Process in batches of 5 with max 2 concurrent batches
$results = await(batch(
$userIds,
function ($batchOfIds) {
return async(function() use ($batchOfIds) {
$queryString = implode(',', $batchOfIds);
return fetch("https://api.example.com/users?ids={$queryString}");
});
},
5, // Batch size
2 // Concurrency
));
// Process results from each batch
foreach ($results as $response) {
$batchUsers = $response->json();
echo "Processed batch with " . count($batchUsers) . " users\n";
}
Timeout Handling
You can add timeouts to promises:
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 occurred: " . $e->getMessage();
}
// Or using the handler
$handler = fetch_client()->getHandler();
try {
$response = $handler->awaitPromise($promise, 5.0); // 5 second timeout
} catch (\RuntimeException $e) {
echo "Timeout: " . $e->getMessage();
}
Retry Handling
For operations that might fail, you can use the retry()
function:
use function retry;
$result = 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);
}
));
// Process the successful response
$data = $result->json();
Advanced Promise Patterns
Promise Chaining
You can chain promises to transform values or perform sequential operations:
async(function() {
return fetch('https://api.example.com/users');
})
->then(function ($response) {
return $response->json();
})
->then(function ($users) {
// Filter users
return array_filter($users, function ($user) {
return $user['active'] === true;
});
})
->then(function ($activeUsers) {
// Extract emails
return array_map(function ($user) {
return $user['email'];
}, $activeUsers);
})
->then(function ($emails) {
echo "Active user emails: " . implode(', ', $emails);
});
Error Handling with try/catch
Using await()
allows for traditional try/catch error handling:
await(async(function() {
try {
$response = await(async(function() {
return fetch('https://api.example.com/users');
}));
if ($response->failed()) {
throw new \Exception("API error: " . $response->status());
}
$users = $response->json();
if (empty($users)) {
throw new \Exception("No users found");
}
return $users[0]['name'];
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
return "Unknown user";
}
}));
Dynamic Promise Creation
You can create promises dynamically based on previous results:
await(async(function() {
// Get all users
$usersResponse = await(async(function() {
return fetch('https://api.example.com/users');
}));
$users = $usersResponse->json();
// Create an array of promises for each user's posts
$promises = [];
foreach ($users as $user) {
$userId = $user['id'];
$promises[$userId] = async(function() use ($userId) {
return fetch("https://api.example.com/users/{$userId}/posts");
});
}
// Execute all promises concurrently
$postResponses = await(all($promises));
// Process the results
$userPosts = [];
foreach ($postResponses as $userId => $response) {
$userPosts[$userId] = $response->json();
}
return $userPosts;
}));
Working with Specific Response Methods
The Response
class in Fetch PHP provides many helpful methods that work well with promises:
await(async(function() {
$response = await(async(fn() => fetch('https://api.example.com/users/1')));
// Check if successful
if ($response->successful()) {
$user = $response->json();
echo "User: {$user['name']}\n";
// Check specific properties
if ($response->hasJsonContent()) {
// Content is JSON
}
// Check status codes
if ($response->isOk()) {
// Status is 200 OK
} else if ($response->isNotFound()) {
// Status is 404 Not Found
}
}
}));
Best Practices
Use async/await for Readability: The async/await pattern makes asynchronous code more readable by making it look like synchronous code.
php// Instead of nested then() callbacks: await(async(function() { $response = await(async(fn() => fetch('https://api.example.com/users'))); $users = $response->json(); // Process users directly }));
Always Handle Errors: Use try/catch with await or catch() with promises to handle errors.
Avoid Nesting: Use async/await to avoid the "callback hell" or "pyramid of doom" problem.
Manage Concurrency: Use
map()
orbatch()
with reasonable concurrency limits to avoid server overload.Control Timeouts: Set appropriate timeouts with the
timeout()
function to prevent operations from hanging.Use Promise Combinators: Leverage
all()
,race()
, andany()
for managing multiple concurrent operations.Use Type-Safe Enums with Responses: Take advantage of response methods like
statusEnum()
for type safety.phpuse Fetch\Enum\Status; await(async(function() { $response = await(async(fn() => fetch('https://api.example.com/users'))); if ($response->statusEnum() === Status::OK) { // Status is exactly 200 OK } }));
Consider Memory Usage: Be mindful of memory usage when working with large datasets.
Debugging Async/Await Code
Debugging asynchronous code can be challenging. Here are some tips:
Break Complex Operations: Split complex async operations into smaller steps.
Add Logging: Log interim results to track the flow of execution.
phpawait(async(function() { echo "Fetching users...\n"; $response = await(async(fn() => fetch('https://api.example.com/users'))); echo "Processing response...\n"; $users = $response->json(); echo "Found " . count($users) . " users\n"; return $users; }));
Use try/catch Blocks: Place try/catch blocks around specific operations to catch errors at their source.
Check Promise States: If things aren't working as expected, check if promises are resolving or rejecting.
Use the Handler's Debug Method: The ClientHandler's
debug()
method can provide useful information.php$handler = fetch_client()->getHandler(); $debugInfo = $handler->debug(); print_r($debugInfo);
Next Steps
- Explore Asynchronous Requests for practical examples
- Learn about Error Handling in asynchronous code
- See Retry Handling for making async requests more resilient
- Check out Type-Safe Enums for working with HTTP concepts