Verse Wiki — the Verse handbook for Blueprint authors
Advanced · EXTRA

Cancellation and defer: the Other Half of Structured Concurrency

Blueprints have no such thing as "cancel a running stretch of logic" — all you can do is raise a flag and let it notice on its own. Verse's race really does cancel the loser, but not instantly: it waits until its next "waiting node" to exit quietly, and defer (that "must-run-before-leaving" wire) still runs. Understand where cancellation happens and when defer backstops it, and you're finally holding every chip structured concurrency has to offer.

1. Cancellation Happens at Suspension Points

First, an intuition to correct: being cancelled is not being cut off mid-stride. Verse tasks are "cooperative" — they only hand over execution at waiting nodes (Sleep, Await, and friends), so cancellation can only land on those nodes too. If a task that just lost a race is stuck on the Delay that is Sleep(5.0), that's its exit moment: the nodes after the Sleep never run, and the task dissolves on the spot.

The value of this rule is predictability: a task is never torn apart "mid-action" — the run of nodes you place between two "waiting nodes" either runs as a whole or not at all, so state consistency comes for free. The official Time Flow docs and the Unreal Fest 2023 concurrency talk both hammer this point repeatedly — structured concurrency's cancellation is automatic and predictable; you never wire up any "kill the task" logic, you just pick the right expression.

2. defer: the Paperwork You Must File on the Way Out

Knowing "it will be cancelled" isn't enough — a cancelled task may be holding unfinished business: half-updated UI, an effect left glowing, a state switch still claimed. Verse's answer is the defer: block — it runs when you leave the stretch of graph it lives in, whether that's finishing normally or being cancelled midway; no distinction. That makes defer the standard tool for concurrency cleanup: restore the UI, unbind events, hand back state — pile it all into defer and nothing gets to weasel out. Let's run an experiment and watch exactly when the loser's defer fires:

cancel_lab_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

cancel_lab_device := class(creative_device):

    SlowWorker()<suspends>:void =
        defer:
            Print("SlowWorker: cleanup done (runs even when cancelled)")
        Print("SlowWorker: starting work")
        Sleep(5.0)
        Print("SlowWorker: all done")    # this line never prints

    FastWorker()<suspends>:void =
        Sleep(1.0)
        Print("FastWorker: first across the line")

    OnBegin<override>()<suspends>:void =
        race:
            SlowWorker()
            FastWorker()
        Print("race over, main line continues")

Read the graph first: inside SlowWorker, the very first thing at the top is a defer (registering the "must-run-before-leaving" cleanup wire), then Print String "starting work", a 5-second Delay, and finally Print String "all done". FastWorker just Delays 1 second and Print Strings. OnBegin drops both functions into one race.

Trace the log order. When the race starts, both wires run in one go to their first "waiting node", top to bottom: SlowWorker registers its defer, prints "starting work", and parks at Sleep(5.0); FastWorker parks at Sleep(1.0). At the 1-second mark, FastWorker crosses the line and wins; SlowWorker is stuck on its Sleep at that moment — cancellation lands, the registered defer pays out, "cleanup done" hits the screen; and "all done" never sees the light of day. Finally the main line prints "race over".

An easy-to-miss detail: a defer must be reached first (registration complete) before leaving that stretch of graph can trigger it. Place the defer before the first "waiting node" — parked at the top of the function, like above — and the cleanup is guaranteed to run no matter which waiting node the task dies at.

3. Four Expressions, Four Answers to "Who Owns the Lifetime"

Put cancellation semantics back into the big picture and you'll see the main lesson's five expressions are really five answers to a single question — who owns this task wire's lifespan: race manages it itself — the winner settles the match and losers are cancelled at their waiting nodes; sync manages it itself too — nobody leaves until everyone's done; branch and rush hand it to the surrounding graph — the graph exits, and the unfinished wires are revoked; spawn answers "nobody" — the task governs itself and lives until its own work is done. Choosing an expression is really answering "how long should this wire live, and who walks it out". See it at this level and the official "prefer structured over spawn" advice stops being dogma and becomes plain truth: letting the graph manage life and death always beats chasing loose horses yourself.

Does the defer block inside a race loser's function run?

This page is distilled from the official docs and an Unreal Fest 2023 talk: Time Flow and Concurrency in Verse ↗, Structured Concurrency ↗, Verse Concurrency — Time Flow (Unreal Fest 2023) ↗.