phaserjs/phaser · error · Error

TimerEvent infinite loop created via zero delay

Error message

TimerEvent infinite loop created via zero delay

What it means

Thrown by Phaser.Time.Clock#addEvent when you re-add an existing TimerEvent instance whose delay is 0 (or negative) while it is configured to repeat. The guard prevents a timer that would fire every frame forever (repeatCount is 999999999999 for loop:true or repeat:-1), which would lock the game loop. The library treats zero-delay repeating timers as a programmer error rather than silently hanging.

Source

Thrown at src/time/Clock.js:216

     * @return {Phaser.Time.TimerEvent} The Timer Event which was created, or passed in.
     */
    addEvent: function (config)
    {
        var event;

        if (config instanceof TimerEvent)
        {
            event = config;

            this.removeEvent(event);

            event.elapsed = event.startAt;
            event.hasDispatched = false;
            event.repeatCount = (event.repeat === -1 || event.loop) ? 999999999999 : event.repeat;

            if (event.delay <= 0 && event.repeatCount > 0)
            {
                throw new Error('TimerEvent infinite loop created via zero delay');
            }
        }
        else
        {
            event = new TimerEvent(config);
        }

        this._pendingInsertion.push(event);

        return event;
    },

    /**
     * Creates a Timer Event and adds it to the Clock at the start of the frame.
     *
     * This is a shortcut for {@link #addEvent} which can be shorter and is compatible with the syntax of the GreenSock Animation Platform (GSAP).
     *
     * @method Phaser.Time.Clock#delayedCall

View on GitHub (pinned to 41be1e462b)

Solutions

  1. Set a positive delay (e.g. delay: 16 for ~60fps, or the frame time in ms) on the TimerEvent before calling addEvent.
  2. If you genuinely want a single immediate callback, set loop:false and repeat:0 (or omit them) so repeatCount is 0 and the guard is skipped.
  3. If you want a per-frame callback, use scene.events.on('preupdate', fn) or scene.sys.events instead of a zero-delay looping timer.
  4. If re-adding an existing instance, ensure its delay/startAt were not zeroed out by a prior reset.

Example fix

// before
this.time.addEvent(new Phaser.Time.TimerEvent({ delay: 0, loop: true, callback: tick }));

// after
this.time.addEvent(new Phaser.Time.TimerEvent({ delay: 16, loop: true, callback: tick }));
Defensive patterns

Strategy: validation

Validate before calling

function safeReAddEvent(clock, event) {
  var repeatCount = (event.repeat === -1 || event.loop) ? Infinity : event.repeat;
  if (event.delay <= 0 && repeatCount > 0) {
    event.delay = clock.scene.sys.game.loop.targetFps ? 1000 / clock.scene.sys.game.loop.targetFps : 16;
  }
  return clock.addEvent(event);
}

Type guard

function isValidTimerEvent(event) {
  var rc = (event.repeat === -1 || event.loop) ? 1 : event.repeat;
  return event.delay > 0 || rc === 0;
}

Try / catch

try {
  this.time.addEvent(existingEvent);
} catch (e) {
  if (e.message === 'TimerEvent infinite loop created via zero delay') {
    existingEvent.delay = 16;
    this.time.addEvent(existingEvent);
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling this.time.addEvent(existingTimerEvent) where existingTimerEvent.delay <= 0 AND (existingTimerEvent.loop === true OR existingTimerEvent.repeat === -1 OR existingTimerEvent.repeat > 0). The branch only runs when the config argument is already a TimerEvent instance (not a plain config object); the same object passed as a config would instead hit TimerEvent.reset (error 41).

Common situations: Re-using/recycling a TimerEvent instance that was previously created with delay:0 and loop:true (common for a per-frame tick). Copy-pasting a config object into a TimerEvent via `new Phaser.Time.TimerEvent(config)` then passing it to addEvent. Migrating from delayedCall (which allows delay 0 for a one-shot) to a looping addEvent and forgetting to bump the delay.

Related errors


AI-assisted analysis of phaserjs/phaser@41be1e462b (2026-08-13). Data as JSON: /api/errors/b615acadd3da3b2e. Report an issue: GitHub.