exprv4:how-to:use_timers_safely

How To Use Timers Safely

Use `timer(callback)` when Expr code needs to run repeatedly after a time interval.

Timers are useful for polling status, updating a dialog, checking a serial device, or running a short periodic task. They are also easy to misuse because the callback runs later, outside the original expression flow.

Start With A Limited Timer

For learning and testing, use `start(intervalMs, count)` so the timer stops by itself.

function Tick(count)
{
    print('tick ', count);
}
 
my_timer = timer(Tick);
my_timer.start(1000, 5);       // five callbacks, one second apart

The callback receives one argument: the timer call count. The first callback receives `1`.

A limited timer is safer than a continuous timer while you are testing because it cannot keep running forever by accident.


Use A Continuous Timer Deliberately

`start(intervalMs)` starts a continuous timer.

function Tick(count)
{
    print('tick ', count);
}
 
my_timer = timer(Tick);
my_timer.start(1000);

Keep the timer object in a variable so you can stop it later.

if (my_timer.is_running())
{
    my_timer.stop();
}

Avoid this when you need later control:

timer(Tick).start(1000);       // hard to stop or inspect later

Keep Timer Work Short

A timer callback should do a small amount of work and return.

Good callback work:

  • update a counter
  • check a flag
  • read a small status value
  • print a short debug message
  • trigger the next small step

Avoid slow or blocking work in a timer callback:

function Tick(count)
{
    // Avoid long loops, blocking dialogs, or slow file work here.
    print('tick ', count);
}

If a task is large, split it into small steps and do one step per timer call.


Keep State In An Object

For anything more than a simple test, keep timer state in an object.

class Monitor()
{
    ticks = 0;
    active = true;
 
    function Tick(count)
    {
        ticks = count;
 
        if (!active)
        {
            return;
        }
 
        print('monitor tick ', ticks);
    }
}
 
monitor = Monitor();
monitor_timer = timer(monitor.Tick);
monitor_timer.start(1000);

This keeps the callback and its state together. It also avoids many root variables such as `monitor_ticks`, `monitor_active`, and `monitor_last_value`.


Stop Timers From Normal Script Code

Prefer stopping a timer from normal script code, a dialog button, or a cleanup step.

function Tick(count)
{
    print('tick ', count);
}
 
my_timer = timer(Tick);
my_timer.start(1000);
 
// later
my_timer.stop();

For a known number of callbacks, use `start(intervalMs, count)` instead of stopping from inside the callback.

my_timer = timer(Tick);
my_timer.start(500, 10);       // stops after ten callbacks

This keeps the timer lifecycle clear.


Use A Timer With A Dialog

A timer can update dialog state while the dialog is open.

Store the window, controls, and timer in one object.

class StatusDialog()
{
    wnd = none();
    status_label = none();
    update_timer = none();
 
    function Show()
    {
        status_label = label()
            .position(20, 20)
            .size(200, 24)
            .text('Waiting');
 
        wnd = window()
            .title('Status')
            .size(260, 110)
            .buttons(false, false, true)
            .on_closed(this.OnClosed);
 
        wnd.add(status_label);
        wnd.show();
 
        update_timer = timer(this.Update);
        update_timer.start(1000);
    }
 
    function Update(count)
    {
        status_label.text('Tick ' + count.to_string());
    }
 
    function OnClosed(code)
    {
        if (!update_timer.is_none() && update_timer.is_running())
        {
            update_timer.stop();
        }
    }
}
 
dlg = StatusDialog();
dlg.Show();

The `on_closed` callback stops the timer when the window closes.


Watch Session Lifetime

A timer remembers the session that was active when the timer was created. The callback runs in that session.

If that session is deleted while the timer is still running, the timer stops.

Do not create a timer in a temporary session and then delete that session while the timer is still needed.

old_session = session.current;
job_session = 'timer_job';
 
session.create(job_session);
session.set_variable(job_session, 'old_session', old_session);
session.join(job_session);
 
function Tick(count)
{
    print('tick ', count);
}
 
job_timer = timer(Tick);
job_timer.start(1000, 3);
 
// Wait for the timer to finish, or stop it, before deleting this session.
 
session.join(old_session);
session.delete(job_session);

For long-lived timers, create them in a long-lived session and keep their objects there.


Do Not Copy Timer Objects Between Sessions

`timer` objects are not cloneable. They own active timer state and a callback reference.

Do not expect to move a timer object to another session with `session.set_variable(…)`.

Instead, create and stop the timer in the session that owns the callback.

function Tick(count)
{
    print('tick ', count);
}
 
my_timer = timer(Tick);
my_timer.start(1000);

Keep normal data, such as counters and settings, in cloneable values or object fields. Keep the live timer object local to its owning session.


Handle Callback Errors

If a timer callback raises an error, the timer prints a timer callback error message and stops.

Validate values inside the callback before using them.

class Poller()
{
    divisor = 1;
 
    function Tick(count)
    {
        if (divisor == 0)
        {
            print('invalid divisor');
            return;
        }
 
        value = count / divisor;
        print(value);
    }
}
 
poller = Poller();
my_timer = timer(poller.Tick);
my_timer.start(1000);

Use `print()` while developing timer callbacks. It is easier to see why a timer stopped.


Choose A Reasonable Interval

A very small interval can create unnecessary load, especially if the callback does host work.

Use an interval that matches the job:

  • status text update: `500` to `1000` ms
  • periodic check: `1000` ms or more when possible
  • short retry sequence: use a limited count

Avoid using a timer as a tight loop. If you need a calculation loop, use normal loop syntax in one expression. If you need periodic host interaction, use a timer with a practical interval.


Common Mistakes

Calling the callback instead of passing it:

my_timer = timer(Tick());      // wrong
my_timer = timer(Tick);        // correct

Creating a continuous timer during testing:

my_timer.start(1000);          // runs until stopped
my_timer.start(1000, 5);       // safer while testing

Deleting the timer session while the timer is still needed:

// Stop the timer before deleting the session that owns it.
my_timer.stop();

Doing too much work in the callback:

function Tick(count)
{
    // Keep this short.
}

Use a class for long-lived timers:

class TimerJob()
{
    t = none();
    count = 0;
 
    function Start()
    {
        t = timer(this.Tick);
        t.start(1000);
    }
 
    function Stop()
    {
        if (!t.is_none() && t.is_running())
        {
            t.stop();
        }
    }
 
    function Tick(timer_count)
    {
        count = timer_count;
        print('tick ', count);
    }
}
 
job = TimerJob();
job.Start();
 
// later
job.Stop();

This keeps the timer, callback, and state in one object.


See Also

exprv4/how-to/use_timers_safely.txt · Last modified: by andrej

Page Tools