A deadline is a stage
Timeout.after(duration) is a Drive value, so it attaches to a function with >>. Each call races the function's body against a Clock deadline. A body that finishes first returns its value; otherwise the call raises :timeout. The stage adds :timeout to the row and Clock to the effects.
type TimeoutProbe = {
mut events: Vector(String);
}
let recordrecord: fn(probe: Book.Concurrency.Timeout.TimeoutProbe @ global, event: String @ global) -> Unit = fn(probeprobe: Book.Concurrency.Timeout.TimeoutProbe, eventevent: String) {
probe.events := Vector.pushpush: pure fn!('a)(items: Vector(String), item: String) -> Vector(String)(probeprobe: Book.Concurrency.Timeout.TimeoutProbe.eventsevents: Vector(String), eventevent: String);
};
let epochepoch: Instant = must Instant.parse_rfc3339parse_rfc3339: pure fn(string: String) -> Instant, exn [ :parse(String) ]("2026-01-01T00:00:00Z""2026-01-01T00:00:00Z": String);
let slow_answerslow_answer: fn(delay: Duration @ global) -> Int, exn [ :timeout ] = fn(delaydelay: Duration: Duration) {
Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(delaydelay: Duration);
4242: Int
} >> Timeout.afterafter: pure fn!(duration: Duration) -> Timeout.After(Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int));
Body Wins
provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
expect ✓try slow_answerslow_answer: fn(delay: Duration @ global) -> Int, exn [ :timeout ](Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int)) {
| :timeout -> 00: Int
} == 4242: Int;
expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int);
};Deadline Wins
provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
expect ✓try slow_answerslow_answer: fn(delay: Duration @ global) -> Int, exn [ :timeout ](Duration.secondsseconds: pure fn!(value: Int) -> Duration(99: Int)) {
| :timeout -> 00: Int
} == 00: Int;
expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int);
};The loser is interrupted
The body runs on its own fiber. When the deadline wins, that fiber is interrupted: its cleanup runs and it never resumes. Because the body outlives the call's own frame, everything it captures must be @ global.
let guardedguarded: fn(probe: Book.Concurrency.Timeout.TimeoutProbe @ global) -> Unit, exn [ :timeout ] = fn(probeprobe: Book.Concurrency.Timeout.TimeoutProbe: TimeoutProbe @ global) {
defer recordrecord: fn(probe: Book.Concurrency.Timeout.TimeoutProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timeout.TimeoutProbe, "cleaned""cleaned": String);
Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.hourshours: pure fn!(value: Int) -> Duration(11: Int));
recordrecord: fn(probe: Book.Concurrency.Timeout.TimeoutProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timeout.TimeoutProbe, "finished""finished": String);
} >> Timeout.afterafter: pure fn!(duration: Duration) -> Timeout.After(Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int));
Loser Runs Cleanup
let probeprobe: Book.Concurrency.Timeout.TimeoutProbe: TimeoutProbe @ global = TimeoutProbe { events = []; };
provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
expect ✓match guardedguarded: fn(probe: Book.Concurrency.Timeout.TimeoutProbe @ global) -> Unit, exn [ :timeout ](probeprobe: Book.Concurrency.Timeout.TimeoutProbe) {
| _ -> "finished""finished": String
| error :timeout -> "timeout""timeout": String
} == "timeout""timeout": String;
};
expect ✓probeprobe: Book.Concurrency.Timeout.TimeoutProbe.eventsevents: Vector(String) == ["cleaned""cleaned": String];Errors the body raises propagate as they would without the stage.
let failingfailing: fn() -> Int, exn [ 'a | :busy | :timeout ], eff 'b = fn() {
error :busy;
00: Int
} >> Timeout.afterafter: pure fn!(duration: Duration) -> Timeout.After(Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int));
Body Errors Propagate
provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
expect ✓match failingfailing: fn() -> Int, exn [ :busy | :timeout ]() {
| valuevalue: Int -> valuevalue: Int
| error :busy -> 11: Int
| error :timeout -> 22: Int
} == 11: Int;
};