22 · Temporal Expressions
Scheduling: every, after, and timeout
Periodic loop with `every`, one-shot firing with `after`, and deadline cancellation with `timeout`.
On this page
Zolo has three scheduling constructs built into the language. All are
reserved keywords — do not use every, after, or timeout as variable
names; prefer interval, scheduled, deadline, or limit instead.
every — periodic loop
every <dur> { ... } repeats the body at the given interval. It is an infinite
loop by design: terminate it with break when the exit condition is reached.
A counter with break after 3 iterations; a running sum that stops once the
desired total is reached.
// Feature: periodic loop with `every <dur> { ... }`
// Syntax: `every <duration> { body }` — repeats the body at the given
// interval. It is a keyword (not a variable name).
// When to use: health-checks, polling, animation, metrics, any
// task that needs to run repeatedly on a fixed interval.
var tick = 0
// Runs 3 times (short interval for the demo) and exits with `break`.
every 50ms {
tick = tick + 1
print("tick #{tick}")
if tick >= 3 {
break
}
}
print("done")
// expected:
// tick #1
// tick #2
// tick #3
// done
// Another example: summing until a total is reached.
var total = 0
every 25ms {
total = total + 10
if total >= 30 {
break
}
}
print("total = {total}")
// expected: total = 30
after — one-shot firing
after <dur> { ... } schedules the block to run exactly once, after the delay.
It is fire-and-forget: it does not block the main flow. Use sleep
afterwards to keep the program alive until the callback fires.
Three after calls coexisting; firing order determined by the scheduler.
// Feature: schedule one execution with `after <dur> { ... }`
// Syntax: `after <duration> { body }` — fire-and-forget; the block
// runs **once** after the delay. It is a keyword.
// When to use: one-shot callbacks, timers, expirations, single
// reminders. Unlike `every`, it does not repeat.
print("scheduling...")
// Block runs 50ms later — does not block the main flow here.
after 50ms {
print("fired after 50ms!")
}
// Multiple `after` may coexist.
after 100ms {
print("fired after 100ms!")
}
after 25ms {
print("this one was first")
}
print("scheduled, waiting...")
// Keep the program alive long enough to see the firings.
sleep 200ms
print("end")
// expected (approximate order — depends on the scheduler):
// scheduling...
// scheduled, waiting...
// this one was first
// fired after 50ms!
// fired after 100ms!
// end
timeout — deadline cancellation
timeout <dur> { ... } returns an object { ok, value, error }. If the block
completes within the deadline, ok is true and value holds the return.
If it expires, ok is false and error is "timeout". Because the
scheduler is cooperative, cancellation only occurs at yield points (sleep, IO,
etc.).
Happy path with a return within the deadline; note on cooperative behaviour and an alternative name for the deadline variable.
// Feature: cancel work that takes too long with `timeout`
// Syntax: `timeout <dur> { body }` returns `Result<T, TimeoutError>`.
// When to use: network calls, uncertain IO, tasks that need a
// fail-safe. Cooperative: only fires at a yield point.
// Happy path: body finishes within the deadline.
let result_ok = timeout 100ms {
return 42
}
match result_ok {
Result::Ok(value) => print("ok: {value}"),
Result::Err(error) => print("err: {error.message}"),
}
// expected: ok: 42
// IMPORTANT: `timeout` is a reserved keyword — you cannot use it
// as a variable name. Use `deadline`, `limit`, `dur`, etc. instead.
let limit = 5000
print("using '{limit}' instead of 'timeout' as the name")
// Timeout case: body takes longer than the limit and is interrupted.
// Note: cooperative — the deadline is checked each time the body
// yields (e.g. via `sleep`), so slow work needs checkpoints: a body
// that never yields, or yields once and then finishes, runs to
// completion and returns Ok. A yielding body also needs a task to
// yield to, so run the block inside `spawn` (the top-level main
// thread cannot yield); the task prints after the main chunk ends.
spawn {
let result_slow = timeout 30ms {
for _ in 0..10 {
sleep 20ms // each sleep is a checkpoint where the deadline can fire
}
return "too late"
}
match result_slow {
Result::Ok(value) => print("ok: {value}"),
Result::Err(error) => print("err: {error.message}"),
}
}
// expected: err: timeout
Challenge
Combine every 100ms with an after 500ms that sets a stop flag. The every
should check the flag and call break when it is true.
See also