Device Lifecycle: OnBegin, OnEnd, and the Swallowed Coroutine
In the main lesson, OnBegin was just “the function called when the game starts”. This page pulls the camera back to take in a device’s entire life: waking up in OnBegin, taking a bow in OnEnd — and that intriguing warning in the official docs: “coroutines spawned in OnEnd may never run”.
In the main lesson, OnBegin was just “the function that fires automatically when the game starts” — the device’s Event BeginPlay. This page pulls the camera back to take in a device’s entire life: waking up in OnBegin, taking a bow in OnEnd — and that intriguing warning in the official docs: “coroutines spawned in OnEnd may never run” (spawn = branching off a fresh exec wire on the fly; a coroutine = an exec wire that can pause and resume at will and lives as long as the session).
1. Two Ends of a Life: OnBegin Opens, OnEnd Takes the Bow
A creative_device’s lifecycle is framed by two overridable functions:
A creative_device’s lifecycle is framed by two functions you can override (rewrite via a Blueprint’s Override menu):
OnBegin<override>()<suspends>:void =— runs when the game experience begins. The official API page describes it as “override this function to add custom logic when the game experience begins”;OnEnd<override>():void =— runs when the game experience ends: the right place for wrap-up work — printing a final-score log, recording end state, and the like.
Set the two signatures side by side and one glaring asymmetry jumps out: OnBegin carries <suspends>; OnEnd does not. That is no typo — it is design intent stated plainly. The opening function is built to wait at leisure, loop, and span the whole game; the curtain-call function is built to “say its line and leave”: the game is switching off the lights and locking up, with no time left for any async waiting.
Set the two lines side by side and one glaring asymmetry jumps out: OnBegin carries <suspends> (the waiting badge); OnEnd does not. That is no typo — it is design intent stated plainly. The opening function is built to wait at leisure (place a Delay), loop, and span the whole game; the curtain-call function is built to “say its line and leave”: the game is switching off the lights and locking up, with no time left for any waiting.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Watch a device's life: opening, heartbeat, curtain call
lifecycle_device := class(creative_device):
var Beats:int = 0
OnBegin<override>()<suspends>:void =
Print("OnBegin: experience started, device on duty.")
loop:
Sleep(5.0)
set Beats += 1
Print("Heartbeat #{Beats}: OnBegin is still alive.")
if (Beats >= 3):
break
OnEnd<override>():void =
Print("OnEnd: experience over, device takes its bow.")
Reading the graph: this Blueprint first creates an integer variable Beats = 0 in the variables panel. When Event BeginPlay fires, a Print String prints “OnBegin: experience started…”; then it enters a loop (like a While Loop): each lap starts with a Delay of 5 seconds, then a Set node bumps Beats by 1, a Print outputs “Heartbeat #…”, and a Branch checks whether Beats ≥ 3 — once that passes, Break exits the loop. The OnEnd block below is the device’s closing event: it just prints one line, “OnEnd: experience over…”, and that is that.
The loop: in the example prints a “heartbeat” every 5 seconds and exits via break after 3 beats. Remove the counter and the break, and the loop would keep beating for the entire session — perfectly legal, and in fact the common shape for periodic logic (mob spawning, countdowns, scheduled checks). What makes it possible is precisely OnBegin’s <suspends>.
The loop in the example prints a “heartbeat” every 5 seconds and jumps out via break after 3 beats. Remove the counter and the break, and the loop would keep beating for the entire session — perfectly legal, and in fact the common shape for periodic logic (mob spawning, countdowns, scheduled checks). What makes it possible is precisely OnBegin’s <suspends> (the waiting badge).
2. The Mental Model: OnBegin Is a Coroutine That Lives With the Session
Tear down the old notion of “a function gets called once and is over”, and install this mental model instead: a device’s OnBegin is a coroutine — a line of execution that can suspend at any time, resume at any time, and lives as long as the session does.
Tear down the old notion of “an event fires once, runs through, and is over”, and install this mental model instead: a device’s OnBegin is a coroutine — a line of execution that can pause at any time, pick up right where it paused, and lives with the session for as long as it runs (picture a Blueprint exec wire that never loses power, with pause and resume buttons you can hit at will).
When it Sleeps it is not “frozen stiff” — it steps off the stage, and when the time comes it resumes playback right at the pause point; it may loop forever, because it was always allowed to live until the session ends. That “log frozen during Sleep” you watched in the main lesson’s stepper is, up close, exactly this coroutine in its suspended state.
When it Sleeps it is not “frozen stiff” — it steps off the stage, and when the time comes it resumes playback right at the pause point — entirely the feel of a Delay node; it may loop forever (like a While Loop whose condition is always true), because it was always allowed to live until the session ends. That “log frozen during Sleep” you watched in the main lesson’s stepper is, up close, exactly this exec wire with its pause button held down.
This model is the foundation for every advanced play to come: event subscription, timers, the game’s main loop — all of them grow out of the insight that “OnBegin is a long-lived coroutine”. Verse also offers concurrency expressions — spawn, sync, race, rush, branch — for splitting one line of execution into several. All but spawn are structured: the child tasks’ lives are governed by the enclosing scope, and when the scope exits, the tasks clock out with it. Concurrency headlines a later chapter; consider this a seed planted.
This model is the foundation for every advanced play to come: event subscription (Bind Event), timers, the game’s main loop — all of them grow out of the insight that “OnBegin is one long-lived exec wire”. Verse also offers concurrency constructs — spawn, sync, race, rush, branch — for forking one line of execution into several that run at once: spawn forks off a new wire and does not wait for it; sync runs several wires together and continues only once all arrive; race has them sprint — first across the finish line counts, and the rest are cut on the spot. All but spawn are structured: the forked wires answer to the enclosing block, and when the outer one wraps up, they are all swept off stage together (a bit like fencing a group of nodes inside one managed branch — the main line ends, the whole group stops). Concurrency headlines a later chapter; consider this a seed planted.
3. The Swallowed Coroutine: OnEnd’s Official Warning
Now you can make sense of that warning on the official OnEnd API page: “any coroutine spawned in OnEnd may never run”.
Now you can make sense of that warning on the official OnEnd API page: “any coroutine spawned in OnEnd may never run” (the fresh exec wire you spawn off may never get a single turn to execute).
The reason is no mystery: while OnEnd runs, the game experience is winding down — the scheduler is busy striking the set and no longer guarantees stage time for newborn async tasks. Spawning a coroutine in OnEnd is like handing in your act as the theater dims the lights and empties out: the stage is already coming apart, and that act will almost certainly never be scheduled. It does not error — it just vanishes silently, which is harder to track down than any error.
The reason is no mystery: while OnEnd runs, the game experience is winding down — the engine’s task scheduling is busy striking the set and no longer guarantees stage time for newly created waiting tasks. Spawning off a fresh exec wire in OnEnd is like handing in your act as the theater dims the lights and empties out: the stage is already coming apart, and that act will almost certainly never be scheduled. It does not error — it just vanishes silently, which is harder to track down than any error.
Field rules:
- In OnEnd, do only synchronous, instantly-finished work: print logs, record state;
- In OnEnd, do only work that finishes on the spot, with no waiting: print logs, record state;
- Don’t spawn coroutines in OnEnd, and don’t expect any “do it in a moment” logic to be honored after the curtain falls;
- Don’t spawn in OnEnd (branching off a fresh exec wire on the fly), and don’t expect any “do it in a moment” logic to be honored after the curtain falls;
- Anything that needs to run long-term belongs in OnBegin, the long-lived coroutine.
- Anything that needs to run long-term belongs in OnBegin, the long-lived exec wire.
Why do the official docs warn “don’t count on coroutines spawned in OnEnd”?Why do the official docs warn “don’t count on a spawn in OnEnd (a fresh exec wire branched off on the fly)”?
OnBegin carries <suspends> while OnEnd does not. What intent does that design convey?
4. Sources
This page is distilled from official Epic documentation: