{"record":{"id":"f00c6cd24a425d56","repo":"guzzle/promises","slug":"cannot-reject-a-rejected-promise","errorCode":null,"errorMessage":"Cannot reject a rejected promise","messagePattern":"Cannot reject a rejected promise","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"src/RejectedPromise.php","lineNumber":112,"sourceCode":"        }\n\n        return null;\n    }\n\n    public function getState(): string\n    {\n        return self::REJECTED;\n    }\n\n    public function resolve($value = null): void\n    {\n        throw new \\LogicException('Cannot resolve a rejected promise');\n    }\n\n    public function reject($reason): void\n    {\n        if ($reason !== $this->reason) {\n            throw new \\LogicException('Cannot reject a rejected promise');\n        }\n    }\n\n    public function cancel(): void\n    {\n        // pass\n    }\n}\n","sourceCodeStart":94,"sourceCodeEnd":121,"githubUrl":"https://github.com/guzzle/promises/blob/42118e66a53c492effaf92bc357e931985d5c6f9/src/RejectedPromise.php#L94-L121","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$rejected->reject(new TimeoutException('timeout')); // new instance !== stored reason\n// after\nstatic $reason; $reason = $reason ?: new TimeoutException('timeout');\n$rejected->reject($reason); // same instance: idempotent no-op","handlingStrategy":"type-guard","validationCode":"// Only safe when re-rejecting with the IDENTICAL reason instance:\nif ($p instanceof \\GuzzleHttp\\Promise\\RejectedPromise && $reason !== $storedReason) {\n    return; // or create a new RejectedPromise($reason)\n}","typeGuard":"function canRejectWith(\\GuzzleHttp\\Promise\\PromiseInterface $p, $reason): bool\n{\n    if (!$p instanceof \\GuzzleHttp\\Promise\\RejectedPromise) {\n        return $p instanceof \\GuzzleHttp\\Promise\\Promise && $p->getState() === 'pending';\n    }\n    return false; // identity comparison with the stored reason is not externally checkable; re-reject only via the same $reason reference\n}","tryCatchPattern":"try {\n    $promise->reject($reason);\n} catch (\\LogicException $e) {\n    // settled with a different reason; keep the original rejection\n}","preventionTips":["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."],"tags":["promises","state","immutability","guzzle"],"backgroundTag":"invalid-state-transition","analyzedSha":"42118e66a53c492effaf92bc357e931985d5c6f9","analyzedAt":"2026-09-14T00:10:14.865Z","contentChangedAt":"2026-09-14T00:10:14.865Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}