Quick answer: Object.InstantiateAsync integrates results during the main-thread Update window. If you await it from a non-Unity thread or destroy the calling MonoBehaviour before integration, the completion callback is dropped. Use await on the main thread or attach to completed from a long-lived owner.
Here is how to fix Unity 2023+ InstantiateAsync calls whose completion callback never runs. You spawn 100 prefabs via the new async API expecting them to drip into the scene over a few frames, but no callback fires and the prefabs never appear. The new API has specific lifecycle requirements that are subtler than classic Instantiate.
The Symptom
Calling InstantiateAsync(prefab, count) returns an AsyncInstantiateOperation. You attach a Completed handler. Time passes; nothing happens. The handler never runs. The Hierarchy stays empty. op.IsDone is false but never becomes true.
What Causes This
Integration off-thread. Awaiting on a Task scheduler that does not return to the main thread skips the integration window. The operation completes but the result is never integrated.
Owner destroyed. If the MonoBehaviour that called InstantiateAsync is destroyed before integration completes, the handler may be cleaned up alongside it.
Time scale 0. The operation requires Unity to advance the simulation. With Time.timeScale = 0, Update may still run but the operation queue may delay integration until time resumes.
Exception in batch. If one prefab in the batch throws during instantiation, the remaining batch may be aborted depending on Unity version.
The Fix
Step 1: Await on the main thread.
using System.Threading.Tasks;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject prefab;
async void SpawnWave(int count)
{
var op = Object.InstantiateAsync(prefab, count);
await op; // continues on main thread
foreach (var go in op.Result)
go.transform.position = RandomPos();
}
}
The await keyword respects Unity’s synchronization context. Continuation runs on the main thread, where integration completes the operation.
Step 2: Use the Completed event for callbacks.
var op = Object.InstantiateAsync(prefab, count);
op.completed += handle => {
foreach (var go in op.Result) ConfigureSpawnedEnemy(go);
};
Step 3: Confirm the caller survives. If the calling MonoBehaviour is destroyed before completion, the callback is invalidated. Spawn from a long-lived manager object (autoload, scene-persistent service):
public class SpawnService : MonoBehaviour
{
public static SpawnService Instance;
void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
public async Task<GameObject[]> SpawnAsync(GameObject prefab, int count)
{
var op = Object.InstantiateAsync(prefab, count);
await op;
return op.Result;
}
}
Step 4: Watch out for time scale 0. If your game pauses via Time.timeScale = 0 and you spawn during pause, integration may pause too. Either spawn at timeScale = 1, or use Realtime callbacks (Update with unscaled time) to drive completion polling.
Step 5: Catch exceptions. Wrap your continuation in try/catch to surface batch failures:
try
{
await op;
}
catch (System.Exception e)
{
Debug.LogError($"InstantiateAsync failed: {e}");
}
When To Use Async vs Sync Instantiate
Async wins when you spawn many prefabs at once (level transitions, enemy waves) and want the cost spread across frames. Sync wins for single, immediate spawns where you need the object on the same frame (projectiles, hit effects). Mixing both is fine; choose per call site based on count and urgency.
“Async returns to the main thread for integration. Stay on the main thread, keep the caller alive, and the callback fires.”
Related Issues
For Addressables async instantiation, see Addressables Failed To Load. For async loading hangs, see Async Scene Loading Callbacks.
Await on main thread. Long-lived owner. Try/catch the await. The operation completes.