sebastianbergmann/phpunit · error · InvalidArgumentException

Value for %s must not be negative.

Error message

Value for %s must not be negative.

What it means

HRTime is PHPUnit's immutable high-resolution timestamp value object (built on hrtime()); its constructor rejects negative seconds or nanoseconds. The Facade uses it for stopwatch starts and for SystemStopWatchWithOffset in process isolation, so the error typically comes from user code computing a negative difference or offset and feeding it to HRTime::fromSecondsAndNanoseconds().

Source

Thrown at src/Event/Value/Telemetry/HRTime.php:91

        if ($seconds < 0) {
            return Duration::fromSecondsAndNanoseconds(0, 0);
        }

        return Duration::fromSecondsAndNanoseconds(
            $seconds,
            $nanoseconds,
        );
    }

    /**
     * @phpstan-assert non-negative-int $value
     *
     * @throws InvalidArgumentException
     */
    private function ensureNotNegative(int $value, string $type): void
    {
        if ($value < 0) {
            throw new InvalidArgumentException(
                sprintf(
                    'Value for %s must not be negative.',
                    $type,
                ),
            );
        }
    }

    /**
     * @throws InvalidArgumentException
     */
    private function ensureNanoSecondsInRange(int $nanoseconds): void
    {
        if ($nanoseconds > 999999999) {
            throw new InvalidArgumentException(
                'Value for nanoseconds must not be greater than 999999999.',
            );
        }

View on GitHub (pinned to f123cdb2a2)

Solutions

  1. Order the operands: subtract the earlier reading from the later one and verify with $later >= $earlier before constructing.
  2. Use HRTime::duration($start) for differences; it borrows across the second boundary and clamps at zero instead of throwing.
  3. Keep seconds and nanoseconds as the split hrtime(false) pair (or split hrtime(true) totals with intdiv) rather than mixing clock types.

Example fix

// before
$stamp = HRTime::fromSecondsAndNanoseconds($t0Sec - $t1Sec, $t0Nanos - $t1Nanos); // may be negative

// after
$stamp = HRTime::fromSecondsAndNanoseconds($t1Sec, $t1Nanos);
$elapsed = $stamp->duration(HRTime::fromSecondsAndNanoseconds($t0Sec, $t0Nanos));
Defensive patterns

Strategy: validation

Validate before calling

[$seconds, $nanoseconds] = hrtime(false); // both non-negative by contract

if ($seconds < 0 || $nanoseconds < 0) {
    throw new LogicException('hrtime() returned unexpected negative values');
}

$stamp = HRTime::fromSecondsAndNanoseconds($seconds, $nanoseconds);

Type guard

function isNonNegativeTimeComponent(int $value): bool
{
    return $value >= 0;
}

Prevention

When it happens

Trigger: HRTime::fromSecondsAndNanoseconds($t1 - $t0, ...) where $t1 < $t0 (swapped hrtime readings); computing an offset for a child process whose clock reading is behind the parent's; casting a negative float from hrtime(false) arithmetic to int.

Common situations: Extensions capturing their own hrtime() anchors around tests that run in isolated processes; custom stopwatch implementations; 32-bit platforms where nanosecond totals overflow.

Related errors


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/faeb5dd87f14d587. Report an issue: GitHub.