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

Why return Is Banned in decides Functions

A developer copied a failable Find function straight from the official docs — and it failed to compile. Pressed for answers, an Epic employee delivered the explanation from the language-design level. This page unpacks the forum cold case: the confiscated return, the ambitions of lenient evaluation, and the officially blessed alternative.

1. A Forum Cold Case: Copied From the Docs, Still Won’t Compile

There’s a well-traveled thread on the Epic developer forums: “Has anyone successfully written a failable function in Verse?”. What the poster did could not have been more ordinary — following the official docs of the time, they wrote a Find function with <decides> that searches an array for an element and returns it once found. The code looked roughly like this:

find_wrong.verse
# How not to do it: return inside a <decides> function body - compile error
FindBigger(Items:[]int, Target:int)<transacts><decides>:int =
    for (Item : Items):
        if (Item > Target):
            return Item    # ← the compiler stops you right here

First, read the snippet: it’s a for / ForEach loop flipping through the array Items, and the moment it finds something bigger than Target, it wants to grab it with return — take the result and jump straight out of the function. A perfectly natural idea — and it’s exactly that return the compiler stops dead.

Intuitively there’s nothing wrong here: found it, leave early with the loot — standard practice in nearly every mainstream language. But the Verse compiler rejects it without mercy. The poster briefly questioned reality — the example came straight out of the docs, after all. Then Epic employee DiG3 gave the direct answer in the thread: you cannot explicitly return from a failable context. Remember Lesson 12’s list? The body of a function with the <decides> effect is one big failure context — so the ban covers the entire function body. The thread even got the official docs example fixed — a nice little tale of the community pushing documentation quality forward.

2. The Official Explanation: Lenient Evaluation and the Confiscated return

“You can’t” is just the rule; “why you can’t” is this page’s main course. Epic employee UltimateLambda gave the language-level reason in the same thread: Verse intends to support a mechanism called lenient evaluation — plainly put, the engine is allowed to rearrange the order of computations that don’t affect each other, or even set them aside until the result is actually needed. It’s the groundwork Verse is laying for “running massive amounts of logic at once”.

Now put the two actors on the same stage: return says “stop right now, I’m leaving”, and its meaning depends entirely on “executing strictly in order”; the speculative execution of <decides> (remember Lesson 12’s “shadow graph”?) says “this whole stretch of mine might be voided and rolled back to try again”, and with lenient evaluation layered on, the engine may shuffle the actual order of the steps too. Both want to command the timeline: let them mix, and “when does it count as failed, when does it count as returned, how far does the rollback rewind” all become impossible to say. The language designers made one clean cut — the combination is simply forbidden, and break likewise. What the compiler intercepts isn’t a slip of your fingers; it’s an entire class of programs whose meaning just can’t be stated clearly.

3. The Officially Blessed Alternative: Collect With an option

The ban is in place, but the job still needs doing. The idiomatic replacement given in the thread: take an option variable (typed ?int — a little “has a value / empty” box) to collect the result, let the for / ForEach filter condition stand in for “early exit”, and finally use ? to lift the value out of the box as the function’s parting result:

find_right.verse
# The right move: collect in an option + brake in the filter clause + query to finish
FindBigger(Items:[]int, Target:int)<transacts><decides>:int =
    var Found:?int = false
    for (Item : Items, Item > Target, not Found?):
        set Found = option{Item}
    Found?

Step through the dance: var Found:?int = false first readies an empty option box as the “loot bag”; the ForEach’s three clauses form a chain of checks — Item : Items supplies the goods one by one, Item > Target does the filtering, and not Found? is the brake pad: as soon as the bag holds something, every following round’s check chain stops going through and the loop body no longer runs (a gentler kind of “early exit”). The last line, Found?, is the finishing touch: it is itself one more check — if the bag holds a value, take it out as the function’s result; if the bag is empty, this grab fails, and since the whole function body is a failure context, the entire function fails, and the if (X := FindBigger[Items, 5]) that called it gracefully takes else. Not once did “not found” resort to a magic number or an error.

Note <transacts><decides> appearing as a pair in the signature (Lesson 12’s rule), and the square-bracket call FindBigger[Items, 5]. This “collect with an option” pattern deserves a spot in your muscle memory — it’s the standard answer to every “exit early with a value once found” need in Verse.

Inside a <decides> function, you want “find it and leave early with the value”. The officially blessed idiom is?

4. Sources and Further Reading

This page is compiled from the original Epic developer forum thread and the replies from Epic employees DiG3 and UltimateLambda. For the firsthand wording and the follow-up discussion, head to the thread:

Has anyone successfully written a failable function in Verse? — Epic Developer Forums ↗

If you’re curious why lenient evaluation is worth all these syntax sacrifices to Verse, the neighboring extra page “Verse’s Theoretical Roots: The Verse Calculus” picks up exactly where this leaves off — that paper is the theoretical bedrock beneath these design trade-offs.