Deep Dive · EXTRA
defer and Async Cancellation: The Bug That Outlasted Two New Years
When race cancels a function, registered defers may never run — a teardown of the famous bug report, plus rules of self-defense.
Enter the extra →ForEach Loop can only run a known number of laps. This lesson unlocks a true perpetual-motion machine: loop for unconditional repetition, break as the emergency exit, Sleep as the heartbeat — plus a dedicated cleanup specialist called defer. By the end you'll be writing loops that run an entire game session, and you'll know how to keep them from freezing your device.
Last lesson's ForEach Loop node is like running a fixed number of laps around a track: however many elements the Array holds, the Loop Body runs once per element, then stops on its own. But games are full of jobs where nobody knows how long they'll run — patrolling guards, contraptions cycling forever, round after round of gameplay. Those jobs go to loop: the closest Blueprint picture is wiring an exec line from the tail of a node back around to its own start, spinning lap after lap unconditionally — no condition check anywhere, no asking whether to keep going.
So how do you stop it? The language gives you two exits: break jumps out of the current loop; return is more drastic — it ends the entire Blueprint function on the spot (and can hand a value to the function's Return output pin on the way out). Note that break only exits the innermost loop — in nested loops, when the inner one shouts break, the outer one keeps right on spinning.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
lap_counter_device := class(creative_device):
OnBegin<override>()<suspends>:void =
var Lap:int = 0
loop:
set Lap += 1
Print("Lap {Lap}")
if (Lap >= 3):
break
Print("Finished {Lap} laps — across the line!")
A loop doesn't count its own laps, so we create an integer variable Lap in the variables panel. Picture the snippet above as a node graph: each time around, a Set node first adds 1 to Lap (set Lap += 1), then Print String prints a "Lap N" line; next, a Branch checks whether Lap >= 3 holds — if so, the True pin leads to a Break node that jumps off the loop wire; once out, the Print String after the loop announces the finish line.While we're here, let's settle a frequent question: if you've seen loop styles like while, do-while, or continue elsewhere, Verse has none of them. To get a "keep spinning only while the condition holds" loop, the recipe is: loop unconditionally, check the condition with a Branch at the top of the body, and the moment it fails, Break out of the loop from the False pin — which is exactly this snippet:
# Other languages: while (Energy > 0) { ... }
# The Verse equivalent:
var Energy:int = 3
loop:
if (Energy > 0):
Print("{Energy} energy left")
set Energy -= 1
else:
break
The mnemonic: ForEach Loop is for one-by-one traversal, loop is for unconditional repetition. The former's lap count comes from the Array's length, so it stops by nature; the latter never stops by nature — whether it stops depends entirely on the exits you install. Which plants a safety question: what if you forget to install one? Section 4's common pitfalls are dedicated to exactly that.
"Break when you count to 3" was just the warm-up. Inside devices, loop's real home turf is loops that run for the entire game session — checking a state every second, spawning a supply drop every 5 seconds, updating a position every frame. These loops deliberately have no break; they catch their breath with Sleep:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
heartbeat_device := class(creative_device):
OnBegin<override>()<suspends>:void =
loop:
Print("Beep — one patrol lap")
Sleep(1.0)
This is "heartbeat mode": do a bit of work, Sleep(1.0) for a second, wake up and carry on — in Blueprint terms, you hang a 1-second Delay node on the tail of the loop body, and when time's up, execution continues from the top of the loop wire.Sleep is the equivalent of Blueprint's Delay — the latent node with the little clock icon that waits a while. Verse uses the <suspends> marker to declare that a function body contains little-clock nodes; any function with one of those inside must be called from an exec line that can also wait, so it has to carry <suspends> as well. Fortunately, this lesson's entry point OnBegin (the equivalent of Event BeginPlay) comes with it built in — nothing for you to worry about. One special usage: Sleep(0.0) doesn't mean "don't sleep" — it means "sleep until the next frame". loop + Sleep(0.0) is a loop that runs once per frame.
More elegant than "ask every second" is "call me when it happens": in Blueprint you already bind events (Bind Event) all the time — here, Await() puts the loop into a dead sleep at this step until that event rings once, and only then does it wake up and move on —
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
door_bell_device := class(creative_device):
@editable
Bell:button_device = button_device{}
OnBegin<override>()<suspends>:void =
loop:
Bell.InteractedWithEvent.Await()
Print("Someone rang the bell!")
Each lap, the loop falls asleep at Await(), wakes up to do its work when the button is pressed, then swings back to the top of the loop and dozes off again. Compared to using Sleep(0.0) to ask "pressed yet? pressed yet?" every frame, loop + Await is far cheaper on performance, and it's the standard idiom for handling repeated events — the same idea as Blueprint's "bind once, and it fires automatically every time after". (When Await() wakes up, it also hands over the player pin of whoever triggered it, much like the Instigator output on an event node; to catch it, write Agent := Bell.InteractedWithEvent.Await().)Behind this "await-events + heartbeat" combo sits Verse's concurrency system — clone jutsu like spawn (fork a new line off the current one) and race (several lines race; first across the finish line wins) get their own teardown in Lesson 24.
Now finish a heartbeat device by hand: the first blank is the word that jumps off the loop wire once we count to 3 (think of the Break node), the second blank is that one-second Delay between beats (think Sleep).
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
three_beats_device := class(creative_device):
var Beats:int = 0
OnBegin<override>()<suspends>:void =
loop:
set Beats += 1
Print("Heartbeat {Beats}")
if (Beats >= 3):
____
____(1.0)
Loops are handled — now meet the cleanup specialist. Lots of game operations come in pairs: open a door, close it later; show UI, hide it later; light a lamp, put it out later. The trouble is the second half often has several exits to cover — it must happen on the normal path, when a Break jumps out, and when the function ends early (return). Re-wire those few cleanup nodes at every exit, miss one, and you have a bug.
defer (deferred execution) exists for exactly this: when the exec line reaches the defer node, it doesn't run that little section right away — it first registers it on a to-do list; when the exec line is about to leave the stretch of graph it lives in — whether by finishing normally, by a Break jumping out, or by the function ending early (return) — the registered section finally catches up. It's like hanging a sign on the doorknob as you walk in — "lock the door before you leave" — and whichever exit you take later, the sign reminds you. Click "Run next step" and watch the order: line 8 registers, and line 9 only runs at the very end:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
vault_device := class(creative_device):
OnBegin<override>()<suspends>:void =
Print("Entering the vault")
defer:
Print("On the way out: lock the door")
Print("Grabbing the gems")
Print("Walking to the vault door")
Click “Run next step” to watch the code execute line by line.
Three rules to lock in: First, a defer only counts as registered once it has been reached — a defer sitting on one of a Branch's output wires registers only if the exec line actually takes that branch; if the exec line leaves the function before ever reaching the defer, it never runs at all. Second, once registered, leaving early through any exit (a Break, or ending the whole function early) triggers it too — many exits sharing one set of cleanup is precisely its value. Third, it is block-level: defer follows only the stretch of graph it lives in, and catches up the moment the exec line leaves that stretch — it doesn't hold out for the whole function to end.
defer and loop also team up: a defer registered inside a loop body runs once per lap, every time execution leaves the loop body —
loop:
defer:
Print("Cleanup for this lap")
Print("Patrol duty")
break # the registered defer still runs before break leaves the loop
There are limits: that little defer section may not contain a return that ends the function, nor a Break that jumps to a loop outside it (though opening your own loop inside the section and Breaking out of that one is allowed).Two more practical reminders: don't put latent, wait-a-while nodes (the little-clock kind) inside a defer; and don't place defer as the final step of a stretch of graph — registering at the very last step is registering for nothing. As for the execution order among multiple defers, the official docs don't say — don't let your cleanup logic depend on it.
This lesson's pitfalls cost more than any before, because when a loop is broken, the Compile button at the top left of your Blueprint won't turn red — it's not something caught at compile time, but a runtime accident that only blows up once the game is running. Accident number one looks like this:
# ✗ Never write this
OnBegin<override>()<suspends>:void =
var Count:int = 0
loop:
set Count += 1
# No break, and no Sleep / Await —
# this frame never finishes
A loop with no Break and no breathing point (a Sleep-style Delay, or an Await on an event) tries to spin infinitely within a single frame. Verse's runtime ships with infinite-loop detection: it cuts the loop off and throws the ErrRuntime_InfiniteLoop runtime error. What really hurts is the officially documented consequence: after a runtime error occurs, all further Verse execution on that device stops — the symptoms are buttons that suddenly go dead and contraptions going on strike, as if the whole level froze over. Picture the accident scene: a player presses the doorbell and nothing happens; you dig through the logs and find the culprit is a completely different loop that forgot its exit. Heartbeat mode is safe precisely because each lap's Sleep spreads the work across many frames. To dig into how this error is detected (and the bizarre cases where you get hit without ever writing an infinite loop), see this lesson's extra page.
The remaining pitfalls, ranked by how often people step in them:
<suspends> — Compile turns red: Sleep is that little-clock Delay and needs the "can wait" capability, and this function never declared that it can wait. Put game loops inside the entry point OnBegin<suspends> (it can wait by birthright), or use spawn to fork a new line to run them (see Lesson 24).Half of this lesson maps onto nodes you already know; the other half — defer — has no Blueprint counterpart at all. Both halves are worth a look.
| What you do in Blueprint | How you write it in Verse | Difference |
|---|---|---|
| A While Loop node with a Boolean on its condition pin | loop: spins unconditionally; check the condition at the top of the body with if and break when it no longer holds |
Verse has no while, no do-while, and no continue; the condition test moves from the loop header into the loop body, and the exit is yours to install |
| A Break node | break |
Same behavior: it leaves only the innermost loop. The difference is that Verse’s break works on loop only — write it inside a for and you get a compile error |
| A Do N node (lets the first N calls through, then shuts) | Keep your own var Lap:int: set Lap += 1, then if (Lap >= 3): break |
Blueprint gives you a ready-made node that carries its own state; Verse gives you nothing, so you declare and increment the counter yourself — three more lines, but the state is out in the open and can’t be “forgotten to Reset” |
| A Do Once node (lets only the first call through) | Keep your own var HasRun:logic = false: do the work inside if (not HasRun?):, then set HasRun = true |
Same story: Blueprint packages “run once” into a node, while Verse asks you to write out the hidden flag variable explicitly |
| A Delay node (the latent one with the little clock icon) | Sleep(1.0) |
Same capability, but Verse requires the calling function to be marked <suspends>; Sleep(0.0) means “sleep until the next frame”, effectively moving the work to the next Tick |
| Bind Event to …: register once, and every later firing runs automatically | Bell.InteractedWithEvent.Await() inside a loop: |
Blueprint registers a callback elsewhere in the graph; Verse parks the loop on that one line and waits — the jump to another corner of the event graph becomes a single line in the main flow, and reads in order |
| Paired cleanup (open/close, show/hide) rewired at every exit | defer: registers once and runs whenever you leave that block |
No Blueprint counterpart: finishing normally, breaking out, or returning early all trigger the same cleanup, and the classic “forgot to wire one of the exits” bug disappears |
The first several rows share one pattern: Blueprint packages common routines into nodes; Verse unpacks them back into a few lines you write yourself. Nodes like Do N and Do Once each hide a state variable you can’t see — convenient, but it remembers the last call across invocations, and debugging it comes down to guesswork. Verse makes you spell that variable out: a few lines longer, but “what is it right now” is always visible.
defer, by contrast, is pure gain — it fixes the bug node graphs leak most: a function with three exits where the cleanup nodes got wired to only two. Blueprint authors usually remember that bug vividly, and defer promotes “remember to clean up” from personal discipline to a language guarantee. The one thing to watch is the caveat in this lesson’s pitfalls: it only counts as registered once execution has actually reached it.
Loops and cleanup are both equipped — three challenge gates to clear. Zero penalty for wrong answers; retry as many times as you like.
A loop has no break, and no Sleep / Await. What happens?
Which statement about defer is correct?
You want to exit an entire ForEach Loop traversal partway through. What is the right approach?
Deep Dive · EXTRA
When race cancels a function, registered defers may never run — a teardown of the famous bug report, plus rules of self-defense.
Enter the extra →Deep Dive · EXTRA
How the runtime decides something is an infinite loop, why you can get hit without writing one, and where to investigate live errors.
Enter the extra →Technique · EXTRA
The global metronome and the periodic contraption — game loops in action from two official tutorials.
Enter the extra →