Skip to content

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.

05-every.zolo
Playground
// 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.

06-after.zolo
Playground
// 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.

07-timeout.zolo
Playground
// 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.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br