Skip to content

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.

04-process-exit.zolo
// 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.

05-on-shutdown.zolo
// 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.

07-on-panic.zolo
// 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.

06-on-signal.zolo
// 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.

enespt-br