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

  1. 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.
  2. Only reject pending promises; keep settlement under a single owner.
  3. Create a new RejectedPromise/Promise for a genuinely different failure outcome.
  4. 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

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


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)