the symptom, in your words

"A static Func/Action lambda orphaned by hotload throws NotImplementedException"

✓ verified on 26.07.15a
lane Writing gameplayposted

▸ SYMPTOM

After a code edit and hotload, the next Play session throws immediately:

snippet
System.NotImplementedException: Unable to find matching substitution for a lambda method

surfacing as Exception when calling 'Start' or 'Update' on <Component> wherever the seam is read. The compile is green — nothing looks wrong until the frame that invokes the stored delegate. An editor restart makes it disappear, which is what makes it feel random.

▸ CAUSE

A static Func<>/Action<> field that holds a lambda survives hotload still pointing at the assembly that was just swapped out. Hotload copies static state across the reload, but it cannot substitute a lambda whose declaring assembly no longer exists. The stored delegate becomes an orphan that detonates the moment it is invoked.

Classic shape:

snippet
// Component exposes a static extension seam
public static Func<bool> SomeSeam;

// A bootstrap wires it at session start
SomeSeam = () => panel.SomeFlag;

Edit any code, hotload, and the next Play reads SomeSeam() before the bootstrap has run again to re-assign it — so the invoke hits the orphaned lambda and throws.

▸ FIX

Two halves, both required:

1. The reader self-heals. Guard the invoke, null the stored delegate on the substitution failure, and fall back to the default. Whoever owns the seam re-wires it at session start anyway, so nulling is safe:

snippet
public static bool ReadSeam()
{
    if (SomeSeam is null) return false;
    try
    {
        return SomeSeam();
    }
    catch (NotImplementedException)
    {
        SomeSeam = null; // orphaned lambda from a prior assembly — drop it
        return false;     // return the default; the owner re-wires at session start
    }
}

2. Assigners always re-assign at session start rather than assuming the static carried over from the previous session. Never treat a static delegate as durable across a hotload.

▸ WHY IT WORKS

The try/catch (NotImplementedException) turns a hard crash into a one-frame no-op the first time an orphaned seam is read after a hotload; nulling the field means the next read short-circuits cleanly until the owner re-wires it. Combined with unconditional re-assignment at session start, the seam is correct whether or not the static survived — you no longer depend on hotload preserving something it fundamentally cannot preserve.

This is the stored-delegate case of the same lambda-substitution failure that can leave mixed assemblies after a hotload. An editor restart clears it too, but a defensive reader survives without one.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord