Skip to content

Timers & Event Loop

StarLang has built-in timer constructs for periodic and delayed execution.

every

Repeating timer. Executes the block at a fixed interval. Returns a timer ID.

every 1000 {
    console.log("tick")
}

timer

One-shot timer. Executes the block once after a delay. Returns a timer ID.

timer 5000 {
    console.log("5 seconds passed")
}

schedule

Delayed repeating timer. Syntax: schedule <delay> every <interval> { block } — waits delay ms, then repeats every interval ms. Returns a timer ID.

schedule 2000 every 3000 {
    console.log("started after 2s, repeats every 3s")
}

runtime.keepAlive()

Enters the event loop. Blocks execution until runtime.exit() is called or all timers have completed.

runtime.keepAlive()
console.log("after event loop")

runtime.exit()

Exits the event loop. Typically called from inside a timer callback.

timer 5000 {
    runtime.exit()
}
runtime.keepAlive()

runtime.cancel(id)

Cancels a timer by its ID. The timer stops firing and is deactivated.

var t:number = every 1000 {
    console.log("tick")
}

timer 5000 {
    runtime.cancel(t)
}
runtime.keepAlive()

Timer IDs

All timer constructs (every, timer, schedule) return a numeric ID. Capture it with a variable to cancel the timer later.

var t:number = every 100 { console.log("tick") }

When used as a standalone statement (without assignment), the ID is discarded automatically.

Full Example

package main

fn main():void {
    var t:number = every 100 {
        console.log("tick")
    }

    timer 500 {
        console.log("cancelling")
        runtime.cancel(t)
    }

    timer 700 {
        console.log("done")
        runtime.exit()
    }

    runtime.keepAlive()
    console.log("event loop ended")
}

Output:

tick
tick
tick
tick
tick
cancelling
done
event loop ended

Notes

  • All time values are in milliseconds
  • Timer callbacks run sequentially (no concurrent execution)
  • Timer callbacks can access enclosing local variables
  • Timer IDs are number type (not i32) — this is intentional for consistency with timer interval values which are also number

Timer Limit

Up to 16 timers can be active simultaneously. Exceeding this limit is a runtime error that halts the program:

# 17th timer → runtime error: "too many timers"

On ESP32, 16 is sufficient for most use cases. If you need more, cancel inactive timers with runtime.cancel() to free slots.

keepAlive Requirement

Without runtime.keepAlive(), the program exits immediately after main returns — registered timers never fire:

fn main():void {
    every 1000 { console.log("tick") }
    # no keepAlive → program exits, no ticks printed
}

Always call runtime.keepAlive() after registering timers.

Runtime Guards

Use runtime.guard() to protect against runaway timer callbacks (infinite loops, memory exhaustion). See runtime.guard() for details.

runtime.guard(100000, 0, 0)    # kill script after 100k instructions without yield

Error Handling in Callbacks

An unhandled raise inside a timer callback stops the event loop and halts the program. Use try inside callbacks to catch errors gracefully:

every 1000 {
    var res:dict = try readSensor()
    if (res["failed"]) {
        console.log("sensor error, skipping")
    }
}