Quick answer: LoadAssetAsync returns null in packaged builds when the asset was not cooked. Common causes are a missing primary asset rule, a soft reference that the cook trace cannot follow, an unresolved redirector, or a folder placed under Content/EditorOnly. Add a primary asset rule, list the folder in Additional Asset Directories to Cook, run -run=ResavePackages -fixupredirects, and load via FStreamableManager::RequestAsyncLoad with an explicit priority.
Here is how to fix Unreal’s async load returning null in packaged builds while everything works in PIE. Your inventory system streams item icons on demand. In the editor it works perfectly — the icon appears the instant the player opens the panel. You package the game, run the build, and every async load callback fires with a null pointer. The reference looks identical in both environments. The difference is that PIE loads from a live editor content directory, while the packaged build can only load what the cooker decided to include in the pak files.
The Symptom
An async load that succeeds in PIE returns null in the packaged build. The callback fires (so the request itself is not stuck), but the resolved object is invalid. Three patterns are common:
Some assets load, others do not. Assets directly referenced from a UPROPERTY in a placed actor load fine. Assets only referenced through a soft path constructed at runtime — for example by appending an item id to a base path — come back null.
Editor builds work, shipping builds fail. Both editor and PIE load the asset, including a Development build of the editor opened on the packaged content. A Shipping or Test build returns null for the same path. Shipping is the strictest cook.
Loaded once, never again. The first asset loads but subsequent loads from the same directory return null. This usually means the cooker included one explicitly referenced asset and skipped the rest of the directory.
What Causes This
Primary asset not registered. The Asset Manager only includes assets in the cook automatically when there is a primary asset rule covering them or a hard reference chain it can trace from a known root. Assets that you load by constructed path with no UPROPERTY pointing at them are invisible to the cook unless a rule exists.
Soft reference not in cooked content. A TSoftObjectPtr in source code does not pull its target into the build by itself. The cooker traces hard pointers; soft pointers are dependencies only when something reachable from a primary asset rule references them. A soft pointer hanging off a runtime data table that is itself not cooked is dead.
Redirector missing. When you rename or move an asset, Unreal leaves a redirector behind. If a chain of redirectors exists between the path your code uses and the actual asset location, the cooker may break the chain by not resolving redirectors during cook. The runtime has no redirector to follow and the load fails.
Asset in editor-only directory. Folders named EditorOnly or under Content/Developers are excluded from cooks by default. Assets placed there work in PIE because PIE bypasses cook rules, but they are physically absent from the packaged build.
The Fix
Step 1: Add a primary asset rule. Open Project Settings → Asset Manager and add a new entry under Primary Asset Types To Scan. Set the type name (e.g. Item), point the asset class at your data class (e.g. UItemDefinition), and set the directories to scan to the folder containing your assets. The cooker now treats every asset in those folders as a root and includes them in the build.
// Project DefaultGame.ini equivalent of the asset manager rule
// [/Script/Engine.AssetManagerSettings]
// +PrimaryAssetTypesToScan=(PrimaryAssetType="Item",
// AssetBaseClass=/Script/MyGame.ItemDefinition,
// bHasBlueprintClasses=False,bIsEditorOnly=False,
// Directories=((Path="/Game/Items")),
// Rules=(Priority=-1,bApplyRecursively=True,
// ChunkId=-1,CookRule=AlwaysCook))
// Loading by primary asset id from C++
void UInventorySubsystem::RequestItemIcon(FName ItemId)
{
UAssetManager& Manager = UAssetManager::Get();
const FPrimaryAssetId AssetId(
FPrimaryAssetType(TEXT("Item")), ItemId);
const FSoftObjectPath Path =
Manager.GetPrimaryAssetPath(AssetId);
if (!Path.IsValid()) {
UE_LOG(LogInventory, Warning,
TEXT("No path for item %s — check primary asset rule"),
*ItemId.ToString());
return;
}
TSharedPtr<FStreamableHandle> Handle =
Manager.GetStreamableManager().RequestAsyncLoad(
Path,
FStreamableDelegate::CreateUObject(
this, &UInventorySubsystem::OnItemLoaded,
ItemId),
FStreamableManager::AsyncLoadHighPriority);
PendingHandles.Add(ItemId, Handle);
}
Step 2: Add the directory to Additional Asset Directories to Cook. Even with a primary asset rule, you can belt-and-brace the cook by listing the directory in Project Settings → Packaging → Additional Asset Directories to Cook. The cooker then visits the directory unconditionally, regardless of whether the rule trace finds it.
Step 3: Resolve redirectors. From the editor command line, run:
// Run from a terminal in your project directory
// UnrealEditor.exe MyProject.uproject -run=ResavePackages \
// -fixupredirects -projectonly
// Diagnose redirector chains in C++ (editor-only)
void UEditorUtilSubsystem::FindRedirectors(const FString& Root)
{
#if WITH_EDITOR
FAssetRegistryModule& ARM =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
"AssetRegistry");
FARFilter Filter;
Filter.PackagePaths.Add(*Root);
Filter.bRecursivePaths = true;
Filter.ClassPaths.Add(
UObjectRedirector::StaticClass()->GetClassPathName());
TArray<FAssetData> Found;
ARM.Get().GetAssets(Filter, Found);
for (const FAssetData& A : Found) {
UE_LOG(LogTemp, Warning,
TEXT("Redirector left at: %s"),
*A.PackageName.ToString());
}
#endif
}
The -fixupredirects step rewrites every package in the project to point at the final asset, removing the chain. After running it, commit the resaved packages to source control so other team members and the build farm pick up the fix.
Step 4: Move assets out of editor-only directories. Anything you load at runtime must live in a cookable directory. Move assets out of Content/Developers/<User>, Content/EditorOnly, and any folder excluded by the cook rules. If you must keep editor utilities in those folders, separate runtime data and editor utilities into different folder hierarchies.
Why This Works
Unreal’s cook is a closed-world process. It walks the dependency graph from a set of explicit roots and includes every asset reachable through hard references. Anything outside that graph is excluded. Soft references, runtime path construction, and dynamic data tables all fall outside the trace by default. The Asset Manager exists precisely to solve this problem: a primary asset rule extends the root set so the cooker visits assets that runtime code intends to load.
FStreamableManager::RequestAsyncLoad is the lowest-overhead way to load an asset that has been cooked. The returned handle controls the lifetime — as long as you hold it, the asset stays loaded. When you release the handle and no other references exist, the asset can be unloaded. Wrapper APIs like LoadAssetAsync hide the handle, which is convenient but can lead to subtle lifetime bugs where assets are unloaded between the load completion and your access.
“If your asset works in PIE and not in shipping, the asset is not the bug. The cook rules are. Add a primary asset rule, cook the directory, fix redirectors, and the runtime load will resolve every time.”
Related Issues
If async loads succeed but the asset takes a noticeable time to appear, see Streaming Hitch on Async Load. If a soft pointer evaluates to null even though the asset is cooked, check Soft Pointer Null After Load.
Primary asset rule plus directory cook plus fixupredirects — the three-step cure for the “works in PIE” bug.