briannesbitt/Carbon · error · RuntimeException

Expected positive number of seconds, '.$seconds.' given

Error message

Expected positive number of seconds, '.$seconds.' given

What it means

WrapperClock::sleep() advances (or really sleeps, depending on the wrapped clock) by the given seconds; zero is a permitted no-op but any negative duration is meaningless for sleeping, so it throws RuntimeException. Because WrapperClock can wrap a real Symfony/PSR clock or a frozen DateTime/test factory, this guard protects both real sleep() and simulated time from moving backwards.

Source

Thrown at src/Carbon/WrapperClock.php:122

            ? ($timezone === null ? $now : $now->setTimezone($timezone))
            : $this->dateAsCarbon($now, $timezone);
    }

    private function dateAsCarbon(DateTimeInterface $date, DateTimeZone|string|int|null $timezone): CarbonInterface
    {
        return $date instanceof DateTimeImmutable
            ? new CarbonImmutable($date, $timezone)
            : new Carbon($date, $timezone);
    }

    public function sleep(float|int $seconds): void
    {
        if ($seconds === 0 || $seconds === 0.0) {
            return;
        }

        if ($seconds < 0) {
            throw new RuntimeException('Expected positive number of seconds, '.$seconds.' given');
        }

        if ($this->currentClock instanceof DateTimeInterface) {
            $this->currentClock = $this->addSeconds($this->currentClock, $seconds);

            return;
        }

        if ($this->currentClock instanceof ClockInterface) {
            $this->currentClock->sleep($seconds);

            return;
        }

        $this->currentClock = $this->addSeconds($this->currentClock->now(), $seconds);
    }

    public function withTimeZone(DateTimeZone|string $timezone): static

View on GitHub (pinned to b13f05955d)

Solutions

  1. Clamp the delay before sleeping: $clock->sleep(max(0, $delay))
  2. If the deadline already passed, branch explicitly (skip sleeping, log, or fail) instead of relying on sleep to signal it
  3. Compute delays as float seconds from a single now() reading taken as late as possible to shrink the race window
  4. Catch RuntimeException around third-party sleep calls and treat it as 'deadline passed' if clamping is not acceptable

Example fix

// before
$clock->sleep($resumeAt - $clock->now()->getTimestamp());

// after
$delay = max(0, $resumeAt - $clock->now()->getTimestamp());
if ($delay === 0 && $resumeAt < $clock->now()->getTimestamp()) {
    // deadline already passed - decide explicitly
}
$clock->sleep($delay);
Defensive patterns

Strategy: validation

Validate before calling

$delay = $resumeAt - $clock->now()->getTimestamp();
if ($delay < 0) {
    // deadline already passed - skip sleeping and branch explicitly
    return $onAlreadyDue();
}
$clock->sleep($delay);

Type guard

function isNonNegativeDelay(float|int $seconds): bool
{
    return $seconds >= 0;
}

Try / catch

try {
    $clock->sleep($delay);
} catch (\RuntimeException $e) {
    // negative delay means deadline passed mid-computation
    if ($delay < 0) {
        return; // proceed immediately
    }
    throw $e;
}

Prevention

When it happens

Trigger: $clock->sleep(-1) directly; $clock->sleep($releaseAt - Carbon::now()->getTimestamp()) when the release moment already passed (negative difference); retry/backoff helpers computing $deadline - microtime(true) after a slow attempt; passing -0.5 floats from delta calculations; fuzz tests feeding boundary values expecting 0 to be the floor.

Common situations: Rate-limiter or lock-wait code where the target timestamp expired between computing and sleeping; concurrency races that make a computed delay negative; tests switching from a real clock to MockClock/WrapperClock exposing pre-existing negative-delay paths; timeouts configured as 'already elapsed'.

Related errors


AI-assisted analysis of briannesbitt/Carbon@b13f05955d (2026-08-17). Data as JSON: /api/errors/b6e2f3f7da03a7f5. Report an issue: GitHub.