sebastianbergmann/php-timer · error · NoActiveTimerException

Timer::start() has to be called before Timer::stop()

Error message

Timer::start() has to be called before Timer::stop()

What it means

Timer::stop() throws NoActiveTimerException because stop() was called when no start time was recorded — i.e. Timer::start() was never called, or every prior start has already been matched by a stop() call. The timer keeps a stack of start times; when it is empty, there is nothing to measure. This is an internal state/invariant violation, not a runtime environment problem.

Solutions

  1. Always pair every stop() with a preceding start() on the same Timer instance
  2. Check that the code path calling stop() actually executes the corresponding start() (especially in conditionals and try/finally blocks)
  3. Wrap start() in a try/finally so an exception between start() and stop() does not desynchronize pairing, or guard the stop with a flag
  4. If durations are taken from arbitrary code sections, capture Duration at the measurement site rather than relying on a long-lived shared Timer

Example fix

// before
$timer = new Timer();
$timer->stop(); // NoActiveTimerException: never started

// after
$timer = new Timer();
$timer->start();
try {
    // ... measured work ...
} finally {
    $duration = $timer->stop();
}
Defensive patterns

Strategy: try-catch

Validate before calling

if ($timer->state()->isRunning()) {
    $duration = $timer->stop();
}

Try / catch

use SebastianBergmann\Timer\NoActiveTimerException;

try {
    $duration = $timer->stop();
} catch (NoActiveTimerException $e) {
    // no active timer: skip measurement or log and continue
}

Prevention

When it happens

Trigger: Calling Timer::stop() before any Timer::start(); calling stop() twice after a single start(); creating a new Timer instance (or one whose starts were all consumed) and invoking stop() on it; parallel/nested code paths where another component already popped the start time.

Common situations: Refactored code where the start() call was removed or moved behind a conditional that did not execute; exception paths that skip start() but still run the stop() in a finally block; tests instantiating Timer directly and calling stop() first; sharing one Timer across concurrent tasks.

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 sebastianbergmann/php-timer@3efcdcf213 (2026-09-13). Data as JSON: /api/errors/674272480e42068c. Report an issue: GitHub.

Appendix: source

Thrown at src/Timer.php:33

final class Timer
{
    /**
     * @var list<float>
     */
    private array $startTimes = [];

    public function start(): void
    {
        $this->startTimes[] = (float) hrtime(true);
    }

    /**
     * @throws NoActiveTimerException
     */
    public function stop(): Duration
    {
        if ($this->startTimes === []) {
            throw new NoActiveTimerException(
                'Timer::start() has to be called before Timer::stop()',
            );
        }

        return Duration::fromNanoseconds((float) hrtime(true) - array_pop($this->startTimes));
    }
}

View on GitHub (pinned to 3efcdcf213)