Process Lifecycle
Zolo provides four extension points for a CLI process lifecycle: controlled exit
with process.exit, LIFO cleanup with on shutdown, panic capture with
on panic, and signal interception with on signal.
Controlled exit
process.exit(code) terminates the process with the given code and still
fires all on shutdown hooks before exiting — unlike an abrupt abort:
The on shutdown reason hook is registered before process.exit(0). Run
locally with DEMO_MODE=fail to see the exit with code 1.
// Feature: controlled exit — `process.exit(code)`
// Syntax: `process.exit(0)` for success, `!= 0` for failure.
// When to use: a CLI script signaling a code to the shell; pre-flight
// checks that need to abort early. `on shutdown` hooks still run before
// the process exits.
use std::process
use std::env
on shutdown reason {
print("[shutdown] reason: {reason}")
}
let mode = env.get("DEMO_MODE") ?? "ok"
if mode == "fail" {
print("failure mode - exiting with 1")
process.exit(1)
}
print("success mode - exiting with 0")
process.exit(0)
print("unreachable")
// expected:
// success mode - exiting with 0
// [shutdown] reason: exit
Requires the Zolo CLI/host — open in the playground or run locally.
Multiple shutdown hooks
You can declare as many on shutdown blocks as you like. They fire in LIFO
order (the last one declared runs first), which makes it easy to compose
independent cleanup layers:
Three on shutdown blocks — hook 3 runs before 2, which runs before 1.
The reason parameter is optional: omitting it is valid.
// Feature: lifecycle — `on shutdown` (composes in LIFO order)
// Syntax: `on shutdown { ... }` or `on shutdown reason { ... }`
// When to use: clean up resources (close files, flush logs, drop
// connections) regardless of the exit path (normal, exit, panic, signal).
// Multiple blocks compose in LIFO order — the last one declared runs first.
on shutdown {
print("[hook 1] outermost cleanup (declared first, runs LAST)")
}
on shutdown reason {
print("[hook 2] exit reason: {reason}")
}
on shutdown {
print("[hook 3] innermost cleanup (declared last, runs FIRST)")
}
print("main work")
print("done")
// expected:
// main work
// done
// [hook 3] innermost cleanup (declared last, runs FIRST)
// [hook 2] exit reason: normal
// [hook 1] outermost cleanup (declared first, runs LAST)
Requires the Zolo CLI/host — open in the playground or run locally.
Panic capture
on panic e is the last resort before shutdown due to a fatal error: it
receives the message from panic(...) and runs before on shutdown. Use it to
write diagnostic logs or failure metrics:
The panic(...) call is commented out to keep the exit code 0 in the sandbox;
read the comments and uncomment locally to see the full sequence.
// Feature: lifecycle — `on panic` catches uncaught panics
// Syntax: `on panic e { ... }` receives the message; runs BEFORE
// `on shutdown` — last chance to log / dump diagnostics.
// When to use: catch crashes in production (telemetry, dump file),
// turn a panic into a structured log entry.
//
// NOTE: an uncaught panic exits the process with a non-zero status
// AFTER the `on panic` / `on shutdown` hooks fire. To keep this
// example green in test harnesses (which expect exit code 0), the
// `panic(...)` call below is left commented out and only documented.
on panic e {
print("[panic] caught: {e}")
}
on shutdown reason {
print("[shutdown] reason: {reason}")
}
print("before crash")
// panic("something went very wrong")
//
// expected (when the panic line is uncommented):
// before crash
// [panic] caught: something went very wrong
// [shutdown] reason: panic
// (process exits with non-zero status)
print("(panic skipped to keep exit code 0; see comment above)")
Requires the Zolo CLI/host — open in the playground or run locally.
OS signals
on signal SIGINT { ... } registers an asynchronous handler for a signal. On
Windows only SIGINT (Ctrl+C) and SIGTERM (Ctrl+Break) are delivered.
process.raise allows synthesizing the signal in-process for testing:
process.raise("SIGINT") delivers the signal; process.sleep(50) waits for the
signal thread to drain the event before main execution continues.
// Feature: lifecycle — `on signal` for SIGINT / SIGTERM
// Syntax: `on signal SIGINT { ... }`. On Windows, only SIGINT (Ctrl+C)
// and SIGTERM (Ctrl+Break) are delivered.
// When to use: graceful shutdown — interrupt server loops, save
// state, close connections before dying.
use std::process
var fired = 0
on signal SIGINT {
fired = fired + 1
print("SIGINT received (time #{fired})")
}
on shutdown reason {
print("shutdown reason: {reason}")
}
// `process.raise` injects a synthetic signal — useful for testing
// without needing a shell to send Ctrl+C from outside.
print("raising SIGINT...")
process.raise("SIGINT")
// Give the lifecycle thread time to drain the signal.
process.sleep(50)
print("after sleep, fired={fired}")
print("done")
// expected:
// raising SIGINT...
// SIGINT received (time #1)
// after sleep, fired=1
// done
// shutdown reason: normal
Requires the Zolo CLI/host — open in the playground or run locally.
Challenge
Add a second on signal SIGINT that prints "handler 2" and verify that both
fire in declaration order when process.raise("SIGINT") is called.
See also