{"record":{"id":"674272480e42068c","repo":"sebastianbergmann/php-timer","slug":"timer-start-has-to-be-called-before-timer-stop","errorCode":null,"errorMessage":"Timer::start() has to be called before Timer::stop()","messagePattern":"Timer::start\\(\\) has to be called before Timer::stop\\(\\)","errorType":"exception","errorClass":"NoActiveTimerException","httpStatus":null,"severity":"error","filePath":"src/Timer.php","lineNumber":33,"sourceCode":"final class Timer\n{\n    /**\n     * @var list<float>\n     */\n    private array $startTimes = [];\n\n    public function start(): void\n    {\n        $this->startTimes[] = (float) hrtime(true);\n    }\n\n    /**\n     * @throws NoActiveTimerException\n     */\n    public function stop(): Duration\n    {\n        if ($this->startTimes === []) {\n            throw new NoActiveTimerException(\n                'Timer::start() has to be called before Timer::stop()',\n            );\n        }\n\n        return Duration::fromNanoseconds((float) hrtime(true) - array_pop($this->startTimes));\n    }\n}\n","sourceCodeStart":15,"sourceCodeEnd":41,"githubUrl":"https://github.com/sebastianbergmann/php-timer/blob/3efcdcf2132bd0125edb602c1363011dc5434c3b/src/Timer.php#L15-L41","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n$timer = new Timer();\n$timer->stop(); // NoActiveTimerException: never started\n\n// after\n$timer = new Timer();\n$timer->start();\ntry {\n    // ... measured work ...\n} finally {\n    $duration = $timer->stop();\n}","handlingStrategy":"try-catch","validationCode":"if ($timer->state()->isRunning()) {\n    $duration = $timer->stop();\n}\n","typeGuard":null,"tryCatchPattern":"use SebastianBergmann\\Timer\\NoActiveTimerException;\n\ntry {\n    $duration = $timer->stop();\n} catch (NoActiveTimerException $e) {\n    // no active timer: skip measurement or log and continue\n}\n","preventionTips":["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"],"tags":["php","timer","state-error","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"3efcdcf2132bd0125edb602c1363011dc5434c3b","analyzedAt":"2026-09-13T22:57:23.791Z","contentChangedAt":"2026-09-13T22:57:23.791Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}