Verse Wiki — the Verse handbook for Blueprint authors
Chapter 5 · Lesson 18

option & tuple: Chests That Might Be Empty, Parcels That Bundle Values

The backpack (array) and the locker (map) both assume "the thing is definitely there" — but games are full of "might not be": the chest might be empty, the boss might not have dropped anything yet. This lesson introduces option, which gives "might not exist" an official ID card, and tuple, which bundles several values of different types into one parcel and ships them out together.

1. The ?t Option Type: This Chest Might Be Empty

Picture a classic in-game scene: a chest in the corner of the map that might hold a golden key, or might be completely empty. You meet this "maybe there, maybe not" in Blueprints every day: an object reference variable might point at some Actor, or it might be None. Grab a None reference without checking and up pops that all-too-familiar red "Accessed None" message. A lot of approaches cram "empty" and "has a value" into the same slot and hope for the best — and the whole graph ends up riddled with potential Nones and potential blow-ups. Verse refuses to play that game.

Verse's answer is refreshingly direct: bake "might not exist" straight into the variable slot's type. Put a ? in front of any type and you get its "nullable version," officially called an option: ?int is "a slot that might hold an integer, or might be empty," ?string is "a slot that might hold some text, or might be empty" — just like that Blueprint reference variable that might hold an Actor or might be None, except Verse hands that state an official ID card. So how do you write the "empty chest" itself? It's just a false:

option_basics.verse
# ?int: might hold an int, might be empty
var MaybeANumber:?int = false

# ?string: the boss may not have dropped any loot yet
var LastDrop:?string = false

Heads up: this false is not the "no / off" boolean! It's the dedicated spelling for "empty chest" — the chest is built, and nothing has been put in it. It merely looks like, and collides in name with, the Bool false from Blueprints; the system keeps them perfectly straight. In a ?int slot, false means "this is empty right now — the equivalent of None," nothing more. The official docs write it exactly this way: var SavedPlayer:?player = false — "no player saved yet," like a reference variable still parked on None.

The payoff is immediate: one glance at a slot's type tells you whether to guard against None. See ?string (with the question mark) and you know "this might be empty (None) — check before you use it"; see string (no question mark) and you know "this is guaranteed to have a value — wire away." "Might be empty" goes from a promise you keep in your own head to a hard rule enforced at Compile time — forget the check, and Compile in the top-left lights up red on you.

2. Boxing and Unboxing: option{…} and X?

Now that we have the chest, two moves are mandatory: putting things in (constructing) and taking things out (unwrapping).

Boxing (putting something into the chest) uses option{value} — note the brackets are curly {}, same family as array{} and map{} from Lessons 16 and 17. Writing set MaybeANumber = option{42} is a Set node: wire the new value "a box holding 42" into the variable. Better yet, inside the curly braces of option{…} sits a "does this go through" check zone (a failure context): drop in an operation that might come up empty-handed, and if it goes through the result gets boxed; if it doesn't, you automatically get the empty box false. For example, option{MyArray[0]} — trying to grab the first element, but the array is empty and that slot doesn't even exist? No problem: no error here, you just collect an empty box, and the wire downstream keeps flowing. It joins the two most annoying things in Blueprints — "the operation failed" and "the value is None" — into one pipeline: got it, it's there; didn't get it, it's empty.

Unboxing (taking the thing out) uses a postfix question mark ?, written MaybeANumber?. But the act of opening the box can itself come up empty (the box might have nothing in it!), so like the array access from Lesson 16, it has to live inside a "does this go through" check zone. The most natural stance is to wrap it in an if — the equivalent of a Branch node in Blueprints:

treasure_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

treasure_device := class(creative_device):

    # Chest slot: might hold a golden key, might be empty
    var MaybeKey:?string = false

    OnBegin<override>()<suspends>:void =
        Print("Start-of-game check: nothing in the chest yet")
        set MaybeKey = option{"Golden Key"}
        if (Key := MaybeKey?):
            Print("Chest opened! Got: {Key}")
        else:
            Print("The chest is empty...")
Output Log

Click "Run Next Step" to watch the code execute line by line.

Commit this move to memory: if (Key := MaybeKey?): — open, check, catch, one Branch does it all. Box has goods: take the true pin, and Key holds the bare value with the wrapping torn off (plain string text, no longer that question-marked box); box is empty: take the else pin, never a crash. Feels exactly like pulling a value out of an array in Lesson 16 with if (X := Items[0]):, right? Not a coincidence — Verse turns every "might not get one" operation into the same "does this wire go through" check, and one Branch covers them all.

Quick hands-on: fill the two key bits of syntax back into this coin-collecting device:

coin_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

coin_device := class(creative_device):

    # Might pick up a coin, might come up empty-handed
    var MaybeCoin:?int = false

    OnBegin<override>()<suspends>:void =
        # Boxing: construct a non-empty option (remember the bracket shape)
        set MaybeCoin = ____{99}
        # Unboxing: unwrapping takes a postfix symbol
        if (Coin := MaybeCoin____):
            Print("Picked up {Coin} coins")

3. tuple: Bundle Several Values into One Parcel

On to a different pain point. You want to build a function that spits out a spawn point — an X (float), a Y (float), plus a location name (text). Here's the rub: a Blueprint function can sprout several output pins, but a Verse function returns exactly one value. Surely we don't open a separate variable for each of those three and shuttle notes back and forth? No. Verse's answer is the tuple: bundle several values — of possibly different types (floats and text mixed freely) — into one parcel and pass it around as a single value. Think of it as a small Struct in Blueprints, or a node that pulls out several output pins at once.

The type is written tuple(t1, t2, …), and a concrete value is just parentheses and commas: (1, 2.0, "three"). Remember Lesson 16 warning that mixed cargo like array{1, "a"} makes Compile light up red? The seed planted back then pays off now: arrays demand the same type in every slot; to mix different types, reach for a tuple.

tuple_basics.verse
using { /UnrealEngine.com/Temporary/Diagnostics }

# Mixed parcel: an int, a float, and a string in one package
Loot:tuple(int, float, string) = (3, 0.5, "Healing Potion")

# Multiple return values: just return a tuple
GetSpawnPoint():tuple(float, float) =
    (128.0, 256.0)

ShowLoot():void =
    # Index with parentheses, counting from 0
    Count := Loot(0)
    Name := Loot(2)
    Print("{Name} x{Count}")

    # Catching multiple returns: store the whole parcel, then split by position
    Point := GetSpawnPoint()
    X := Point(0)
    Y := Point(1)
    Print("Spawn point: ({X}, {Y})")

Reading the code: three things happen here. First, Loot is a three-slot parcel holding "3, 0.5, Healing Potion" (a tuple mixing int, float, and string). Second, GetSpawnPoint is an ordinary function that hands back a two-slot parcel (128.0, 256.0) as its return value. Third, inside ShowLoot, Loot(0) and Loot(2) grab values by slot number (like pulling output pins 0 and 2 off the parcel), then the whole GetSpawnPoint() package gets caught and split into X and Y for printing. The parentheses in (0) mean "pull pin number 0."

Three defining personality traits, each more interesting than the last:

1. Access uses parentheses — and never comes up empty. Grabbing from an array is A[0] (square brackets, might miss, must sit in a check zone); grabbing from a tuple is T(0) (parentheses, never misses, write it anywhere). Why such different treatment? Because how many slots a tuple has, and what type sits in each one, are nailed down the moment you Compile — tuple(int, float, string) is always exactly three slots. Write Loot(3) (a fourth slot that doesn't exist) and Compile lights up red on the spot; the game never even gets a chance to run and break. The price: the slot number must be a hard-coded fixed number, not a variable that's only known once the game is running. Stuff a variable i into T(i) and it won't Compile — with each slot a different type, the compiler can't know in advance whether this pull hands you text or a number. Really need a variable index to roam around with? That's a sign what you want is an array, not a tuple.

2. Sealed once packed — even stricter than arrays. There is no set T(0) = X "change one slot" Set — once a tuple is made, no editing individual slots. Want a "change"? Rebuild the whole parcel from scratch and swap it in.

3. This is the official stance for "one function spits out several values." Verse has no "split two in one line" syntax like (A, B) := F() — you catch the whole package into a name first, then split it slot by slot with Result(0), Result(1). That's the standard procedure (much like catching a node's outputs first, then wiring each pin onward in Blueprints). One spoiler while we're here: tuples and "a function's parameter list" are close relatives in Verse — hand a whole tuple package to a multi-input function and it automatically spreads out and files into place. That "one parcel feeds every input" trick gets its own extra page.

4. Common Pitfalls: Crimes of Bracket Shape

Nine out of ten option and tuple pitfalls come down to "bracket shape" and "compared against what." Let's defuse them one by one:

Pitfall 1: boxing with parentheses, option(42). Compile goes red. Boxing accepts exactly one spelling — curly braces: option{42}. The official style guide even suggests: if it's short, a single line option{a} is fine.

Pitfall 2: reaching into a tuple with square brackets, T[0]. Compile refuses. Square brackets [] are array and map territory; tuples only answer to parentheses: T(0). The table below welds the memory in place:

Syntax Used by Can it fail?
A[0] array / map Yes — it can miss; it may only sit in a check zone (a Branch condition)
T(0) tuple No — going past the slot count is a Compile-time red, not a runtime miss
M? option unwrap Yes — it can miss; it may only sit in a check zone (a Branch condition)

Pitfall 3: unboxing without a check zone around it. Write Key := MaybeKey? bare (no Branch / if sheltering it) and Compile goes red with that same old friend: "This invocation calls a function that has the 'decides' effect, which is not allowed by its context." — meaning "this might-miss move isn't allowed to run bare here." We've bumped into it twice in Lessons 12 and 13, and the prescription hasn't changed: move it into an if, into a for's filter condition, or inside option{}.

Pitfall 4: comparing the box itself against a bare value. if (MaybeX = 42) reports a type mismatch: the left side is a "possibly empty box" (?int), the right a plain naked integer (int) — not the same species, no direct comparison. Open the box first, then compare: if (MaybeX? = 42) — the unboxing and the comparison sit in the same check zone; if the box is empty, or the value isn't 42, this wire doesn't go through (off to else). One step, done.

Pitfall 5: dreaming of "split into several in one line." (A, B) := GetXY() — the "catch two at once" move — looks suave in some languages, but in Verse it's a Compile red. Catch the whole package first, then split it slot by slot, like an honest citizen.

Pitfall 6: using a variable that's only decided at runtime as the tuple slot number. T(i) won't Compile — as the previous section said, each slot has a different type, so the compiler must pin down at Compile time which slot you're pulling; it can't wait for the game to be running to find out.

5. Choosing Guide: Picking Among Chapter 5's Four Containers

With that, all four containers of Chapter 5 have arrived. Time for a group photo — plus a quick-reference selection table:

Container Type syntax Literal When to use it
array []int array{1, 2, 3} Variable count, same type, order matters: a squad of monsters, an ammo list
map [string]int map{"A" => 1} Look up values by key: player name to score, item name to price
option ?int false / option{42} 0 or 1 value, "might not exist": the last target hit, a chest that might be empty
tuple tuple(int, string) (1, "Sword") Fixed count, mixed types, travels as one package: multi-value returns, coordinate pairs

The three-question selection chant: Will the count change? Yes — array or map (order matters: array; look up by name: map). Might there be none at all? Yes — option. Just a fixed few, and you want to mix different types? — tuple. Bonus trivia: as long as everything inside is a type that can be compared and tested for equality (officially: comparable), options and tuples become comparable themselves — which means they can turn around and serve as map keys. For example, use tuple(int, int) grid coordinates as the keys of a game-board map. And just like that, the four containers mesh together like gears.

Blueprint Cross-Reference

Every concept in this lesson has a counterpart in Blueprint — except Blueprint keeps them outside the type system, and Verse moved them inside.

What you do in Blueprint How you write it in Verse Difference
An Object Reference variable: might point at an Actor, might be None The ?type option, e.g. var SavedPlayer:?player = false In Blueprint any object reference might be None, and the type doesn't say so; in Verse only a type carrying ? can be empty, and one without it is the compiler's guarantee that a value is there. "Might be empty" goes from a spoken convention to a mark worn on the face
Is Valid node / the Is Valid ? macro: remember to insert a check before you use it if (X := MaybeX?): (unboxing may only live in a failure context) Blueprint's Is Valid is a matter of self-discipline — forget it and the graph still compiles and still ships; in Verse you can't reach the value without unboxing, and unboxing must be wired into a Branch, so forgetting the check simply does not build
Accessed None: red text at runtime, this execution line dies on the spot, every node after it never runs This whole class of error is stopped at Compile time The biggest difference in this lesson: the same bug happens in front of players in Blueprint, and in front of you when you hit Compile in Verse. Verse has no runtime state called "null reference" at all
A function node sprouting several output pins (extra outputs on the Return Node) Return a tuple: GetSpawnPoint():tuple(float, float) Blueprint's multiple outputs are a convenience the node UI provides; text code has exactly one return slot, so Verse puts a parcel in that one slot. Read it with Point(0), Point(1) — and there is no "split into two variables in one line"
Make Struct / Break Struct nodes: bundle several values of different types, then take them apart The tuple's parenthesised construction (1, "Sword") and positional access T(0) Struct members have names; a tuple has only slot numbers, and those numbers must be compile-time constants (T(i) won't compile). Want names? Use a struct — see Lesson 21
Set Members in Struct node: change one member of a struct No counterpart: a tuple is sealed once packed Not even set T(0) = X exists; to "change" it you rebuild the whole parcel — the same value-semantics rule as array and map

Every difference in this lesson grows from one root: Blueprint leaves "might not exist" outside the type system, and Verse carried it inside. In Blueprint an Actor reference has the type Actor; "it might be None" lives in your head, in the team's coding standard, and in the Is Valid node you hope somebody remembers to insert — but the compiler knows nothing about it, so it can't help you. Verse makes ?player and player two different types, and with that, "forgot to check" stops being an oversight and becomes a type error. That is how the entire Accessed None family of bugs gets moved to compile time and killed there.

The tuple half comes from a plainer fact: a node graph can grow as many output pins as it likes, while a line of text code has exactly one return slot. Verse didn't invent special syntax for this; it reused an answer it already had — let that one slot hold a parcel with a fixed count and mixed types. In exchange, you accept that the parcel is read by position rather than by name, and that the position must be fixed at compile time (otherwise the compiler can't know whether this slot is text or a number). If you need names, switch to a struct; if you need a variable index to roam with, what you actually want is an array.

6. Level Challenge

Chests opened, parcels unpacked — three challenge gates to clear. Wrong answers cost nothing; retry as often as you like.

In the line var MaybeCoin:?int = false, what does false mean?

Given T:tuple(int, string) = (7, "Sword"), which of these access spellings compiles?

You want to turn "the might-miss array access Items[0]" into a nullable integer box, ?int (box it if fetched, empty box if not). How do you write it?

Further Reading

Level Up · EXTRA

Writing a Failable Function: <decides> as option's Mirror

No return allowed inside a check zone (a place that can fail) — so how does a "failable function" send its value out? The official option holding-box routine ties this lesson's chest concepts into a closed loop.

Enter the extra →

Technique · EXTRA

Tuple Expansion (Splatting): Pass One Parcel as a Series

Hand a whole tuple parcel to a function as one argument and it automatically spreads out, filing into several inputs — behind it lies Verse's unified worldview that every call is really passing one tuple.

Enter the extra →

Extension · EXTRA

sync Returns a Tuple: Multi-Value Returns in the Concurrent World

The concurrency instruction sync waits for several jobs to all finish, then packs their results into one tuple in the order you wrote them — tuple's "fixed slot count" temperament fits here like a glove.

Enter the extra →