guzzle/promises · error · LogicException

Cannot resolve a fulfilled promise

Error message

Cannot resolve a fulfilled promise

What it means

FulfilledPromise::resolve() may only be called with the identical value the promise already holds (=== comparison); any other value is a no-op that is not allowed. The library throws this LogicException because a settled promise is immutable: silently swapping its value would break the guarantee that every then() observer saw the same result. Note resolve($value) with $value === $this->value is intentionally permitted as a no-op.

Solutions

  1. Do not resolve an already-settled promise; create a new Promise and resolve that instead.
  2. Guard with a state check: only call resolve() on promises that can still be settled.
  3. If you need a different outcome, reject the old promise is also impossible - build a fresh promise chain.
  4. Ensure only one owner/path is responsible for settling each promise.

Example fix

// before
$fulfilled->resolve('different value'); // LogicException
// after
$next = new \GuzzleHttp\Promise\Promise();
$next->resolve('different value');
Defensive patterns

Strategy: try-catch

Validate before calling

// Cannot inspect state publicly on FulfilledPromise; instead ensure the promise is
// freshly created or only settled by a single owner before calling resolve().

Type guard

function canResolve(\GuzzleHttp\Promise\PromiseInterface $p): bool
{
    return $p instanceof \GuzzleHttp\Promise\Promise && $p->getState() === 'pending';
}

Try / catch

try {
    $promise->resolve($value);
} catch (\LogicException $e) {
    // already settled; ignore or create a new promise
    $promise = new \GuzzleHttp\Promise\FulfilledPromise($value);
}

Prevention

When it happens

Trigger: Calling ->resolve($v) on a FulfilledPromise whose stored value is not strictly equal (===) to $v, including the implicit default null when the promise was fulfilled with a non-null value or vice versa.

Common situations: Reusing one promise object across requests and re-resolving it; race conditions where two code paths settle the same promise; refactoring code that assumed promises are mutable; porting code from other promise libraries that ignore re-settling.

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/146c0e3ca6f67339. Report an issue: GitHub.

Appendix: source

Thrown at src/FulfilledPromise.php:98

    public function otherwise(callable $onRejected): PromiseInterface
    {
        return $this->then(null, $onRejected);
    }

    public function wait(bool $unwrap = true)
    {
        return $unwrap ? $this->value : null;
    }

    public function getState(): string
    {
        return self::FULFILLED;
    }

    public function resolve($value = null): void
    {
        if ($value !== $this->value) {
            throw new \LogicException('Cannot resolve a fulfilled promise');
        }
    }

    public function reject($reason): void
    {
        throw new \LogicException('Cannot reject a fulfilled promise');
    }

    public function cancel(): void
    {
        // pass
    }
}

View on GitHub (pinned to 42118e66a5)