sebastianbergmann/phpunit · error · InvalidArgumentException

Value for %s must not be negative.

Error message

Value for %s must not be negative.

What it means

Duration is PHPUnit's immutable elapsed-time value object; its constructor rejects negative components with InvalidArgumentException ('Value for seconds must not be negative.' / '...nanoseconds...'). Note that HRTime::duration() and CpuTime::diff() clamp negative differences to Duration/CpuTime zero, so this error indicates manual subtraction passed to Duration::fromSecondsAndNanoseconds() rather than the built-in difference helpers.

Source

Thrown at src/Event/Value/Telemetry/Duration.php:147

            return true;
        }

        if ($this->seconds < $other->seconds) {
            return false;
        }

        return $this->nanoseconds > $other->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. Use the provided difference APIs, e.g. HRTime::fromSecondsAndNanoseconds(...)->duration($start), which clamps at zero.
  2. Clamp manual results with max(0, $seconds) and max(0, $nanoseconds) before constructing.
  3. Ensure both readings come from the same clock (always hrtime(), never mixing time() with hrtime()) and the same process where possible.

Example fix

// before
$elapsed = Duration::fromSecondsAndNanoseconds(
    $startSec - $endSec,      // negative on swap
    $startNanos - $endNanos,
);

// after
$elapsed = HRTime::fromSecondsAndNanoseconds($endSec, $endNanos)
    ->duration(HRTime::fromSecondsAndNanoseconds($startSec, $startNanos));
Defensive patterns

Strategy: validation

Validate before calling

$seconds     = max(0, $endSec - $startSec);
$nanoseconds = max(0, $endNanos - $startNanos);

$duration = Duration::fromSecondsAndNanoseconds($seconds, $nanoseconds);

Type guard

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

Prevention

When it happens

Trigger: Duration::fromSecondsAndNanoseconds($endSeconds - $startSeconds, ...) with the end timestamp earlier than the start; monotonic-clock reads mixed with wall-clock reads across processes so the difference goes negative; integer underflow on 32-bit PHP.

Common situations: Extensions that report per-test durations computed from hrtime() readings captured in different processes (process isolation); building custom reporters or slow-test detectors; tests with fixture timestamps in the wrong order.

Related errors


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