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

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

Pulling a value from an array can miss; opening an option box can miss — so can a function you build yourself also "possibly fail"? Yes: hang the <decides> marker on it. But the first pitfall appears immediately: inside a "can fail" check zone, you're not allowed to use return to send a value out. Borrowing a real cry for help from the Epic forums, this page walks through the officially recommended option holding-box routine.

1. A Real Cry for Help: Followed the Docs, Still Won't Compile

There's a widely shared post on the Epic developer forums with a title that stings: "Has anyone successfully written a failable function?" The poster, developer alk3ovation, wanted something utterly ordinary — find the first element in an array that meets a condition, hand it over if found, and have the whole function "not go through" if not. Naturally, he wrote a return inside a ForEach-style loop: found it, return, done. Compile shot it down without mercy.

The reason follows straight from Lesson 18: hang <decides> on a function, and the entire function body becomes one big "does this go through" check zone. And Verse forbids explicit return inside such a zone — not a corner case the docs forgot to mention, but a hard rule. So the problem becomes a riddle: no return — then how does "the value we just found" get carried out of the loop and become the result the function hands over?

You already know the answer: option. A ?t slot expresses exactly the two states "not found yet / found some value" — the holding box built for this very moment (empty box = not found yet; loaded box = found it).

2. The Official Pattern: option Holding Box + Last-Line Unwrap

In that thread, Epic staffer DiG3 gave the official answer. Here is its skeleton (that F is a small "checker function" passed in — don't sweat how its type is written just yet; watch how the option box plays courier and carries the value out):

find_pattern.verse
# A failable Find: hand over the element if found, fail as a whole if not
Find(X:[]int, F(:int)<transacts><decides>:void)<transacts><decides>:int =
    # Holding box: starts out empty (false = empty option)
    var Ret:?int = false
    # Three conditions: iterate the elements; F[Y] passes; box still empty (stop at the first hit)
    for (Y : X, F[Y], not Ret?):
        set Ret = option{Y}
    # Last-line unwrap: box has goods => that is the return value; empty box => unwrap fails => whole function fails
    Ret?

Taken apart line by line, every step is a Lesson 18 concept making its combat debut:

var Ret:?int = false — the empty chest takes its position, meaning "nothing found yet."

for (Y : X, F[Y], not Ret?) — this is a ForEach with filter conditions: besides "take each element Y in turn," the for's parentheses can carry "does this go through" checks, and elements that don't pass are simply skipped. Two are hanging here: F[Y] (that little checker function, called with square brackets; fails the check, skip that element) and Ret? (poke the box to see whether it's empty). not Ret? means "keep looking only while the box is still empty" — the moment it's loaded, all remaining elements get skipped. Net effect: "found the first one, stop."

set Ret = option{Y} — found it; box it.

The final Ret? — the master stroke. Verse has "implicit return": whatever you write last in the function body, its result automatically becomes the return value — no need to shout return (like the last node's output wired straight into the Return pin in a Blueprint). And Ret? is a might-miss unboxing move, sitting right in the natural check zone that a <decides> function body is. Box has goods: the bare value that comes out becomes the return value. Box empty: the unboxing misses, and that "didn't go through" travels outward — the whole function counts as failed. "Hand over the value" and "report the failure" handled by the same single line. That is the <decides>-and-option mirror: one writes "might fail" into the function's markers, the other writes "might not exist" into a value's type, and the two convert back and forth through option{…} boxing and ? unboxing. A perfect fit.

Whoever calls it returns to familiar territory: Find carries <decides>, so it must be called with square brackets, wrapped in a check zone (a Branch / if) — if (Result := Find[Numbers, IsBig]):, the same feel as pulling from an array in Lesson 16.

3. Why return Is Banned in Failure Contexts

In the thread, community member UltimateLambda added a layer of design rationale: Verse's long-term roadmap includes "lenient evaluation" — letting the compiler shuffle the order of the operations inside a check zone, or even trial-run them on scratch paper before deciding whether they count (Lesson 16's extra page covered how a check zone that doesn't go through rolls back wholesale, as if never wired — that's part of the same machinery). If the order of operations can be shuffled, then "the moment we hit return, grab the value and bail" — a strictly ordered move — turns ambiguous: which "hitting it" counts? Rather than bury a time bomb, the language simply bans return inside check zones and standardizes on one rule: whatever you wrote last is the return value.

So the restriction isn't spite; it's the price of consistency: in Verse, the code inside a check zone is "one whole block that either goes through together or fails together," not "steps that run one after another and can jump out at any moment." Once that clicks, the option holding-box routine stops being a rote trick and becomes the obvious way: track progress with a box (the option) instead of breaking the flow with a "jump out" (return). From now on, any failable utility you want to build — find a target, check eligibility, roll loot — gets the same three-move combo, copied straight over: start with an empty box, box it when found, open the box on the last line.

Inside a function marked <decides>, you wrap a loop and use an explicit return inside it to send out the found value. What happens?

Sources & Further Reading

Compiled from the Epic developer forums and official documentation: