guzzle/promises · error · LogicException
Cannot reject a rejected promise
Error message
Cannot reject a rejected promise
What it means
RejectedPromise::reject() permits only the no-op case where the new reason is strictly identical (===) to the stored one; any other reason throws this LogicException. This mirrors FulfilledPromise::resolve(): a settled promise keeps its terminal reason, and repeating the exact same rejection is tolerated as an idempotent no-op, but changing the reason would violate immutability.
Solutions
- Reuse the original rejection reason object (keep a reference) when re-rejecting must happen, or skip the call entirely if you know it is settled.
- Only reject pending promises; keep settlement under a single owner.
- Create a new RejectedPromise/Promise for a genuinely different failure outcome.
- Observe failures via ->otherwise() instead of mutating settled promises.
Example fix
// before
$rejected->reject(new TimeoutException('timeout')); // new instance !== stored reason
// after
static $reason; $reason = $reason ?: new TimeoutException('timeout');
$rejected->reject($reason); // same instance: idempotent no-op Defensive patterns
Strategy: type-guard
Validate before calling
// Only safe when re-rejecting with the IDENTICAL reason instance:
if ($p instanceof \GuzzleHttp\Promise\RejectedPromise && $reason !== $storedReason) {
return; // or create a new RejectedPromise($reason)
} Type guard
function canRejectWith(\GuzzleHttp\Promise\PromiseInterface $p, $reason): bool
{
if (!$p instanceof \GuzzleHttp\Promise\RejectedPromise) {
return $p instanceof \GuzzleHttp\Promise\Promise && $p->getState() === 'pending';
}
return false; // identity comparison with the stored reason is not externally checkable; re-reject only via the same $reason reference
} Try / catch
try {
$promise->reject($reason);
} catch (\LogicException $e) {
// settled with a different reason; keep the original rejection
} Prevention
- Keep a single reference to the rejection reason and reuse that exact instance on re-reject.
- Do not construct fresh exception objects when re-rejecting an already-rejected promise.
- Route differing failure outcomes to a new promise instead of mutating the settled one.
When it happens
Trigger: Calling ->reject($r) on a RejectedPromise whose stored reason is not strictly equal (===) to $r - e.g. a different exception instance even with the same message, or the implicit default null where a reason was stored.
Common situations: Error handlers that build a fresh exception for each retry and re-reject the same promise; multiple failure paths racing to settle one promise; wrappers that log-and-reject with a wrapped exception; loops that re-reject per iteration.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot resolve a fulfilled promise
- Cannot reject a fulfilled promise
- Cannot resolve a rejected promise
- Not enough promises to fulfill count
- You cannot create a FulfilledPromise with a promise.
AI-assisted analysis of guzzle/promises@42118e66a5 (2026-09-14).
Data as JSON: /api/errors/f00c6cd24a425d56.
Report an issue: GitHub.
Appendix: source
Thrown at src/RejectedPromise.php:112
}
return null;
}
public function getState(): string
{
return self::REJECTED;
}
public function resolve($value = null): void
{
throw new \LogicException('Cannot resolve a rejected promise');
}
public function reject($reason): void
{
if ($reason !== $this->reason) {
throw new \LogicException('Cannot reject a rejected promise');
}
}
public function cancel(): void
{
// pass
}
}
View on GitHub (pinned to 42118e66a5)