Why Verse Fails #3 — One Rule, Four Costumes
Lesson beginner

Why Verse Fails #3 — One Rule, Four Costumes

beginner Verse

Overview

If you've read anything about Verse before this series, you've probably absorbed a few "rules" about what can't go in an if condition: don't allocate there. Don't build strings there. Don't call functions that make maps there. Hoist everything first. Whole forum threads teach these as separate laws of the language.

Here's what the compiler actually thinks — established by running every one of this lesson's examples on the real UEFN compiler: those were never separate rules, and most of them aren't rules at all. There is ONE rule, and you already learned it last lesson: an unannotated function defaults to no_rollback, and restricted positions — an if condition, a for loop's iterable — only admit rollback-safe work. Everything else is costume.

Today we take the costumes off, one gate-verified probe at a time.

Probe 1 — "you can't allocate in a condition." Really?

Build a price table directly inside the condition — a map literal, right there in the binder list:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vault_g2 := class(creative_device):
    OnBegin<override>()<suspends> : void =
        if (Prices := map{"Rusty Key" => 5, "Gold Key" => 50}, Cost := Prices["Gold Key"]):
            Print("A gold key costs {Cost}")

This compiles. Zero errors. So much for "never allocate in a condition." Construction itself is rollback-safe — Verse can perfectly well throw away a map it just built if the rest of the condition fails.

Maybe the myth means class construction? Probe again:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

key_info := class<concrete>:
    Cost : int = 50

vault_g3 := class(creative_device):
    OnBegin<override>()<suspends> : void =
        if (Info := key_info{}, Info.Cost > 20):
            Print("Expensive key")

Compiles. An archetype instantiation in the condition list is fine too.

Probe 2 — so what WAS failing all this time?

Here's the version of that code that genuinely fails — and look closely at what's different, because it isn't the allocation:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vault_g4 := class(creative_device):
    BuildPrices() : [string]int = map{"Rusty Key" => 5, "Gold Key" => 50}

    OnBegin<override>()<suspends> : void =
        if (Prices := BuildPrices(), Cost := Prices["Gold Key"]):
            Print("A gold key costs {Cost}")
Script error 3512: This invocation calls a function that has the 'no_rollback' effect, which is not allowed by its context.

The map literal moved into a helper — and the helper is unannotated, so it defaults to no_rollback. That's the whole crime. Not the map{}. Not the condition. The silent default from lesson 2, wearing an allocation costume.

Say so, and the "illegal" code becomes legal — same shape, same condition, same allocation:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vault_table_g := class(creative_device):
    BuildPrices()<transacts> : [string]int = map{"Rusty Key" => 5, "Gold Key" => 50}

    OnBegin<override>()<suspends> : void =
        if (Prices := BuildPrices(), Cost := Prices["Gold Key"]):
            Print("A gold key costs {Cost}")

Compiles, zero errors — because BuildPrices now carries <transacts>, a promise a pure map-builder can honestly make. (And yes — the hoisted version you may have been taught also compiles. Hoisting works, but it works incidentally: it moves the unannotated call out of the restricted position instead of fixing the contract. Annotate the helper and put the call wherever reads best.)

Probe 3 — the same costume in a for loop

The restriction isn't only about if. A for loop's iterable position is restricted the same way:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vault_list_h := class(creative_device):
    KeyList() : []string = array{"Rusty Key", "Coral Key", "Gold Key"}

    OnBegin<override>()<suspends> : void =
        for (Name : KeyList()):
            Print("The vault holds: {Name}")
Script error 3512: This invocation calls a function that has the 'no_rollback' effect, which is not allowed by its context.

Same 3512, same 'no_rollback', same diagnosis: KeyList is unannotated. And the same one-word fix:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vault_list_h := class(creative_device):
    KeyList()<transacts> : []string = array{"Rusty Key", "Coral Key", "Gold Key"}

    OnBegin<override>()<suspends> : void =
        for (Name : KeyList()):
            Print("The vault holds: {Name}")

Compiles — the call sits right in the iterable position, legally, because the contract is now stated. (The hoist works here too, and sometimes it's genuinely nicer to read. Choose it for style — never because you think the position is illegal.)

Probe 4 — where the restriction ENDS

One more probe, because knowing where a rule stops matters as much as knowing the rule. case dispatch on an enum — with a real, world-changing device call in the arm:

Compiles. A case arm is NOT a failure context — nothing gets rolled back there — so no_rollback work like granting a real item is perfectly at home. If you've been nervously hoisting device calls out of case arms: you never needed to.

The one true rule

  • Restricted positions (an if's condition list, a for's iterable) admit only rollback-safe work — because a failed attempt there gets undone.
  • The only thing that ever fails the check is an effect contract — almost always the silent no_rollback default on a helper nobody annotated.
  • Allocation was never the crime. Map literals, archetypes, annotated builders — all legal in conditions, gate-verified.
  • Annotate the effect; the position takes care of itself. <transacts> on pure helpers is the real fix. Hoisting is a readability choice, not a repair — when the callee can be annotated.
  • The one exception: NATIVE no_rollback callees. Some engine calls — granting a real item is the gate-proven example — are no_rollback by nature, and no annotation exists to add: the taint inherits upward through every helper that calls them, no matter how innocent your helper looks. When 3512 traces to a native call anywhere down the chain, moving the call out of the restricted position IS the real fix there — relocation, not annotation.
  • How do you tell which case you're in? Ask the compiler. Annotate your helper <transacts> and recompile. If it now passes, the silent default was the whole story. If it still fails 3512, something native down the chain is genuinely no_rollback — relocate. (We ran exactly this test on a random roll while writing this lesson: GetRandomFloat turns out to be rollback-safe — a <transacts> helper calling it compiles, and the roll is legal right in an if condition. A build note of ours had blamed the roll for a 3512 that was really an unannotated helper — the silent default claiming yet another misdiagnosis. The discriminator test caught it before this lesson taught it to you.)
  • Know where restrictions end. case arms, ordinary statements, OnBegin bodies — not failure contexts; no_rollback work belongs there freely.

One honest caveat: a class whose constructor itself does effectful work is a different story than the plain data shapes probed here — when you meet one, read its contract the same way you now read everything else. And when the thing your helper ultimately calls is native no_rollback (an item grant, per the probe above), remember the exception: there's nothing to annotate, and the hoist stops being style and becomes the fix — the discriminator test tells you which world you're in.

Where this goes next

You can now read 3512 in both its faces, name the silent default on sight, and tell a real rule from a folk rule. The series finale turns that skill into a habit: Reading the Compiler — a field guide to the error codes you'll actually meet, and the fix-loop that turns each one into a thirty-second repair.

Get the complete code — free

You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.

Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.

Check your understanding

Test yourself with an interactive quiz and track your progress + earn XP — free for members.

Turn this into a guided course

Add The no_rollback family unified: restricted positions, the silent default, and the allocation myth — retired by gate-verified probes to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.

Original lesson generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.

Comments

    Sign in to vote, comment, or suggest an edit. Sign in