Quick answer: Store the TSharedPtr<FStreamableHandle> from RequestAsyncLoad. In Deinitialize, call Handle->CancelHandle(). Don’t start new loads in EndPlay.
A game with heavy asset streaming hangs on exit. Editor freezes for ~30 seconds before closing. Crash dumps show pending async load on the call stack. The game shut down with loads in flight.
The Pattern
void UMySubsystem::PreloadAssets() {
auto& Manager = UAssetManager::Get().GetStreamableManager();
LoadHandle = Manager.RequestAsyncLoad(SoftPaths, [this]() {
OnLoaded();
});
}
If shutdown happens before completion, the load continues; the captured this may dangle.
Fix: Cancel in Deinitialize
void UMySubsystem::Deinitialize() {
if (LoadHandle.IsValid()) {
LoadHandle->CancelHandle();
LoadHandle.Reset();
}
Super::Deinitialize();
}
Cancel runs synchronously: outstanding load is aborted, callbacks won’t fire. No dangling lambdas.
Use Weak This in Callbacks
TWeakObjectPtr<UMySubsystem> WeakThis(this);
LoadHandle = Manager.RequestAsyncLoad(SoftPaths, [WeakThis]() {
if (UMySubsystem* Self = WeakThis.Get()) {
Self->OnLoaded();
}
});
Defense in depth: even if cancel is missed, the callback won’t crash. WeakObjectPtr nulls out when the object is destroyed.
Don't Start Loads in EndPlay
EndPlay runs during shutdown sequencing. Starting new async loads there guarantees they hang. If you need cleanup-time loads (rare), use sync loads.
Verifying
Stress test: trigger preloads, then exit immediately. Editor closes within 1–2 seconds. Logs show “Cancelled async load: ...” messages. No crash dumps.
“Cancel before shutdown, capture weak. Async loads in flight are guaranteed to outlive the systems that started them.”
For shipping builds especially, the hang appears as the spinning beachball — user experience destroyed by something easy to fix.