Handle cancellation
This guide shows practical patterns for responding to cancellation in your Clawless commands. Each section addresses a specific scenario you might encounter.
Wait for cancellation
The simplest pattern is a command that blocks until the user presses Ctrl+C.
Use cancelled().await to wait asynchronously:
use clawless::prelude::*;
#[derive(Debug, Args)]
pub struct ServeArgs {}
/// Start a server and wait for shutdown
#[command]
pub async fn serve(_args: ServeArgs, context: Context) -> CommandResult {
message!("server started, press Ctrl+C to stop");
context.cancellation().cancelled().await;
message!("shutting down");
Ok(())
}
If the token is already cancelled when you call cancelled().await, the future
completes immediately.
Check cancellation in a loop
For CPU-bound or iterative work, use is_cancelled() to poll the token at
each iteration:
use clawless::prelude::*;
#[derive(Debug, Args)]
pub struct MigrateArgs {}
/// Run database migrations
#[command]
pub async fn migrate(_args: MigrateArgs, context: Context) -> CommandResult {
let cancellation = context.cancellation();
let migrations = discover_migrations()?;
for migration in &migrations {
if cancellation.is_cancelled() {
message!("cancelled, stopping after last completed migration");
break;
}
run_migration(migration)?;
message!("applied: {}", migration.name());
}
Ok(())
}
This pattern ensures each migration either completes fully or isn't started.
Use tokio::select! with cancellation
To race cancellation against an async operation, use tokio::select!. This is
the most common pattern for commands that perform long-running async work:
use clawless::prelude::*;
#[derive(Debug, Args)]
pub struct FetchArgs {
/// URL to fetch
url: String,
}
/// Fetch a URL with cancellation support
#[command]
pub async fn fetch(args: FetchArgs, context: Context) -> CommandResult {
let cancellation = context.cancellation();
tokio::select! {
result = do_fetch(&args.url) => {
let body = result.context("fetch failed")?;
message!("{}", body);
}
_ = cancellation.cancelled() => {
message!("fetch cancelled");
}
}
Ok(())
}
The tokio::select! macro waits for whichever branch completes first. If the
user presses Ctrl+C before the fetch finishes, the cancellation branch runs and
the fetch future is dropped.
Scope cancellation with child tokens
When your command spawns independent sub-tasks, create child tokens so each sub-task can be cancelled independently without stopping the parent:
use clawless::prelude::*;
#[derive(Debug, Args)]
pub struct WatchArgs {}
/// Watch for changes and rebuild on each change
#[command]
pub async fn watch(_args: WatchArgs, context: Context) -> CommandResult {
let cancellation = context.cancellation();
loop {
if cancellation.is_cancelled() {
break;
}
let build_token = cancellation.child();
tokio::select! {
_result = build(&build_token) => {
message!("build completed");
}
_ = wait_for_change() => {
// New change detected, cancel the current build
build_token.cancel();
message!("change detected, restarting build");
}
_ = cancellation.cancelled() => {
message!("shutting down");
}
}
}
Ok(())
}
In this example, each build gets its own child token. When a file change is detected, the current build's token is cancelled without affecting the parent. When the user presses Ctrl+C, the parent token is cancelled and the cancellation cascades to the active build.
See also
- Cancellation - How cancellation works and why Clawless uses cooperative shutdown
- Context - The Context system that provides cancellation to commands