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
- Always pair every stop() with a preceding start() on the same Timer instance
- Check that the code path calling stop() actually executes the corresponding start() (especially in conditionals and try/finally blocks)
- Wrap start() in a try/finally so an exception between start() and stop() does not desynchronize pairing, or guard the stop with a flag
- 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
- Always call start() immediately before the section you measure and stop() in a finally block
- Never call stop() more times than start() was called
- Do not share one Timer instance across independent code paths
- Prefer invoking Timer::time(Closure) which pairs start/stop for you
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
- Cannot determine time at which the request started because…
- Cannot determine time at which the request started because…
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)