pestphp/pest · error · BadMethodCallException

Expectation value is not iterable.

Error message

Expectation value is not iterable.

What it means

expect($value)->each(...) first verifies the expectation value is iterable (array or Traversable). If it is not — a scalar, null, or a plain object — Pest throws BadMethodCallException because each() has nothing to iterate. This is a value-type mistake in the expectation chain, not an assertion failure.

Source

Thrown at src/Expectation.php:187

        return $this;
    }

    /**
     * @return OppositeExpectation<TValue>
     */
    public function not(): OppositeExpectation
    {
        return new OppositeExpectation($this);
    }

    /**
     * @return EachExpectation<TValue>
     */
    public function each(?callable $callback = null): EachExpectation
    {
        if (! is_iterable($this->value)) {
            throw new BadMethodCallException('Expectation value is not iterable.');
        }

        if (is_callable($callback)) {
            foreach ($this->value as $key => $item) {
                $callback(new self($item), $key);
            }
        }

        return new EachExpectation($this);
    }

    /**
     * @template TSequenceValue
     *
     * @param  (callable(self<TValue>, self<string|int>): void)|TSequenceValue  ...$callbacks
     * @return self<TValue>
     */
    public function sequence(mixed ...$callbacks): self

View on GitHub (pinned to 1af74a215c)

Solutions

  1. Assert the value is iterable first, or guard with is_iterable() before the expectation.
  2. Fix the data source: decode JSON with json_decode($json, true) to get an array, or cast (array) $value when appropriate.
  3. If the value is legitimately a scalar, replace each() with a direct assertion such as toBe()/toMatch().
  4. If it can be null, handle the null case separately (toBeNull() or a null-safe branch) before iterating.

Example fix

// before
expect($user->roles)->each(fn ($role) => $role->toBeString());
// after
expect($user->roles)->toBeArray();
expect($user->roles)->each(fn ($role) => $role->toBeString());
Defensive patterns

Strategy: type-guard

Validate before calling

if (! is_iterable($value)) {
    expect($value)->toBeIterableOrArray(); // fails loudly with your own message
    return;
}

Type guard

function isIterable(mixed $value): bool
{
    return is_iterable($value);
}

// usage
if (isIterable($items)) {
    expect($items)->each(fn ($item) => $item->toBeString());
} else {
    expect($items)->toBeNull(); // or whatever the scalar case should be
}

Prevention

When it happens

Trigger: expect('foo')->each(fn ($item) => ...); expect(null)->each(...); expect($object)->each(...) where the object does not implement Traversable (e.g., a plain stdClass or DTO); a function returned null (failed lookup) and the result is fed straight into expect()->each().

Common situations: Chaining ->each() after a collection method that can return null (first(), find()); API responses decoded to objects instead of arrays; refactoring code so a previously-iterable value became a scalar; forgetting that JSON objects decode to stdClass, not array.

Related errors


AI-assisted analysis of pestphp/pest@1af74a215c (2026-08-21). Data as JSON: /api/errors/450f4d77e8233b69. Report an issue: GitHub.