@croven your open question — does Paper validate the spawn world after AsyncPlayerSpawnLocationEvent. ran:, mine, javap -p -c on the 26.3 server jar (Purpur, a Paper fork; this is Paper’s config task): on the consumption path there is no loaded-state check — a grep of the disassembly for isWorldLoaded/World unloaded/isLoaded is 0, and reading the path confirms it.
The flow: lambda$tick$0 runs on CONFIGURATION_POOL (off-main, via supplyAsync) — it builds the event, callEvent() (your handler’s setSpawnLocation + try/catch runs here), returns getSpawnLocation(). Then tick() does eventFuture.join() → the Location → getWorld() → (CraftWorld).getHandle() → stores spawnLevel. That getWorld() — Bukkit’s Location.getWorld(), checking the world reference — is the only gate before the handle is taken.
One scope correction on the 7138901 catch falls out of that. It wraps setSpawnLocation, which clones the Location and checkArguments its world — so it covers a ref already cleared at the handler. But the server’s getWorld() on the joined result runs in tick(), after your handler returns, outside your catch. If the ref clears between the two, the IllegalArgumentException: World unloaded throws in PrepareSpawnTask$Preparing.tick(), not in your catch. And if the world is unloaded but still reachable, that getWorld() passes and its handle is stored as spawnLevel with no loaded-state validation — the silent half you named, no throw on that step.
So the residual is real and the catch is narrower than it reads. I don’t see an in-handler close that doesn’t reintroduce the off-thread lookup (isWorldLoaded is exactly that, as you found). The pre-unload MONITOR null-write shuts the window for any reader that starts after the event; what stays exposed is any invocation that already captured the location before the null-write, including after the handler returns. As you said, that needs a world unload racing a join — an admin or plugin action, not traffic — at which point, as you put it, the lobby’s arguably broken anyway. I’d keep the catch and the null-on-unload as-is, and treat the post-event read as Paper’s to guard, not the plugin’s.