Wait for asynchronous loads with async and await
AsyncOperationHandle provides a Task object that you can use with the C# async and await keywords to sequence code that calls asynchronous methods and handles the results.
The following example loads Addressable assets using a list of keys. The differences between this task-based approach and the coroutine or event-based approaches are in the signature of the calling method. This method must include the async and await keywords with the operation handle's Task property. The calling method, Start in this case, suspends operation while the task finishes. Execution then resumes and the example instantiates all the loaded prefabs in a grid pattern.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
internal class LoadWithTask : MonoBehaviour
{
// Label or address strings to load
public List<string> keys = new List<string>() {"characters", "animals"};
// Operation handle used to load and release assets
AsyncOperationHandle<IList<GameObject>> loadHandle;
public async void Start()
{
loadHandle = Addressables.LoadAssetsAsync<GameObject>(
keys, // Either a single key or a List of keys
addressable =>
{
// Called for every loaded asset
Debug.Log(addressable.name);
}, Addressables.MergeMode.Union, // How to combine multiple labels
false); // Whether to fail if any asset fails to load
// Wait for the operation to finish in the background
await loadHandle.Task;
// Instantiate the results
float x = 0, z = 0;
foreach (var addressable in loadHandle.Result)
{
if (addressable != null)
{
Instantiate<GameObject>(addressable,
new Vector3(x++ * 2.0f, 0, z * 2.0f),
Quaternion.identity,
transform); // make child of this object
if (x > 9)
{
x = 0;
z++;
}
}
}
}
private void OnDestroy()
{
loadHandle.Release();
// Release all the loaded assets associated with loadHandle
// Note that if you do not make loaded addressables a child of this object,
// then you will need to devise another way of releasing the handle when
// all the individual addressables are destroyed.
}
}
When you use Task-based operation handling, you can use the C# Task class methods such as WhenAll to control which operations you run in parallel and which you want to run in sequence. The following example illustrates how to wait for more than one operation to finish before moving onto the next task:
// Load the Prefabs
var prefabOpHandle = Addressables.LoadAssetsAsync<GameObject>(
keys, null, Addressables.MergeMode.Union, false);
// Load a Scene additively
var sceneOpHandle
= Addressables.LoadSceneAsync(nextScene,
UnityEngine.SceneManagement.LoadSceneMode.Additive);
await System.Threading.Tasks.Task.WhenAll(prefabOpHandle.Task, sceneOpHandle.Task);
Note
Awaiting Task never throws - it resolves to default on most failures, though a LoadAssetsAsync call with releaseDependenciesOnFailure: false can return a non-null partial result instead. Check AsyncOperationHandle.Status or OperationException to detect failure this way.
Await an operation handle directly
You can also await an AsyncOperationHandle or AsyncOperationHandle<T> directly, without going through Task. This is built on Unity's Awaitable type and, unlike Task, throws an AsyncOperationHandleException on failure, so a normal try/catch works:
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.Exceptions;
internal class LoadWithAwait : MonoBehaviour
{
public string address;
GameObject m_Instance;
CancellationTokenSource m_Cts;
async void OnEnable()
{
// A fresh token each OnEnable: OnDisable below cancels it, so a load or instantiate
// still in flight when the component is disabled is stopped and its handle released
// automatically - unlike destroyCancellationToken, this also reacts to a mere disable,
// not just final destruction.
m_Cts = new CancellationTokenSource();
try
{
// Unlike handle.Task (resolves to null on failure, never throws), awaiting the
// handle throws AsyncOperationHandleException on failure. A cancellation only
// throws OperationCanceledException if OnDisable runs before the load finishes.
m_Instance = await Addressables.InstantiateAsync(address, transform).ToAwaitable(m_Cts.Token);
}
catch (OperationCanceledException)
{
// Only runs if OnDisable fires before the load finishes; a cancel after success
// just releases the handle without throwing.
}
catch (AsyncOperationHandleException<GameObject> e)
{
// Release immediately rather than waiting for OnDisable to eventually cancel
// m_Cts: the failed handle stays valid (and unreleased) until then, and the
// component could stay enabled indefinitely after a failed load.
Debug.LogError($"Failed to load '{address}': {e.Message}");
e.Handle.Release();
}
}
void OnDisable()
{
// Cancels the pending await (if the instantiate hasn't finished yet) and releases the
// handle - whether it's still pending or already completed - so there is no separate
// cleanup call needed for either case.
m_Cts.Cancel();
m_Cts.Dispose();
}
}
A failed operation's handle isn't released automatically - catch the typed AsyncOperationHandleException<T> (or AsyncOperationHandleException for non-generic handles) and release e.Handle, which is exactly the handle that failed. Release it in the catch block itself; waiting for a later lifecycle event (OnDisable, OnDestroy, a cancellation token) leaves it unreleased until then.
This example keeps the instantiated result alive past the call that created it. Releasing the handle in OnDisable alone isn't enough: OnDisable can run while the load from OnEnable is still pending, and releasing the handle there doesn't stop the await from resuming later against a disabled object.
AsyncOperationHandle.ToAwaitable(CancellationToken) closes that gap: canceling the token always releases the handle, and also throws OperationCanceledException if the load is still pending. If the load already resolved successfully, the cancellation just releases the handle with no throw. OnDisable above cancels a CancellationTokenSource created fresh each OnEnable, so a disable at any point cleans everything up.
Note
OnEnable/OnDisable can run many times over a component's life, so the token must be created fresh each OnEnable and canceled in the matching OnDisable. destroyCancellationToken only cancels on final destruction, so it doesn't fit here - but it's exactly right for a one-shot load, as in the next example.
For a one-shot load started from Start(), ToAwaitable(MonoBehaviour) is simpler: it ties cancellation to destroyCancellationToken for you, so no cleanup method - not even OnDestroy - is needed:
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.Exceptions;
internal class LoadOnceWithAwait : MonoBehaviour
{
public string address;
GameObject m_Instance;
async void Start()
{
try
{
// ToAwaitable(MonoBehaviour) ties cancellation to this object's destruction
// (MonoBehaviour.destroyCancellationToken), so no separate OnDestroy release is
// needed: whether this object is destroyed before or after the instantiate
// finishes, the handle is released automatically.
m_Instance = await Addressables.InstantiateAsync(address, transform).ToAwaitable(this);
}
catch (OperationCanceledException)
{
// Only runs if destruction happens before the load finishes; a cancel after
// success just releases the handle without throwing.
}
catch (AsyncOperationHandleException<GameObject> e)
{
// Release immediately rather than waiting for destroyCancellationToken to
// eventually fire: the failed handle stays valid (and unreleased) until this
// object is actually destroyed, which could be arbitrarily far in the future.
Debug.LogError($"Failed to load '{address}': {e.Message}");
e.Handle.Release();
}
}
}
Note
AsyncOperationHandleException's InnerException is the operation's OperationException. Releasing e.Handle matters even more for LoadAssetsAsync with releaseDependenciesOnFailure: false: it can fail with a partial result (loaded assets alongside null entries), reachable through e.Handle.Result before you release it:
try
{
var loaded = await Addressables.LoadAssetsAsync<GameObject>(locations, null, releaseDependenciesOnFailure: false);
}
catch (AsyncOperationHandleException<IList<GameObject>> e)
{
// e.Handle.Result is the partial list; e.Handle.Status is Failed.
e.Handle.Release();
}
When you load multiple assets with LoadAssetsAsync and don't need to keep them past the call site, you don't need to keep the handle either: Addressables.Release can look up the handle from the result object it returned, so releasing the awaited result in the same scope is enough. Doing the load, use, and release in one method avoids any window where the object could be disabled before the await completes. This only applies on success, though - on failure there's no result to release by, so the catch block releases e.Handle instead:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.Exceptions;
internal class LoadMultipleWithAwait : MonoBehaviour
{
public string label = "characters";
// Load, use, and release in one self-contained scope - no lifecycle callback can run
// before the await completes, so there's no window for a disable to leave it unreleased.
// To keep assets past this method, store the handle instead - see LoadWithAwait.
async void Start()
{
IList<GameObject> loaded = null;
try
{
loaded = await Addressables.LoadAssetsAsync<GameObject>(label);
foreach (var prefab in loaded)
Debug.Log($"Loaded '{prefab.name}' for label '{label}'.");
}
catch (AsyncOperationHandleException<IList<GameObject>> e)
{
// The awaited handle releases itself on failure - only e.Handle needs releasing.
Debug.LogError($"Failed to load label '{label}': {e.Message}");
e.Handle.Release();
}
finally
{
// Addressables.Release(obj) looks up the handle by the exact result object
// returned, so releasing it is enough - releasing a copy (e.g. via .ToList())
// instead logs an error and leaks the real handle.
if (loaded != null)
Addressables.Release(loaded);
}
}
}
Important
Addressables.Release finds the handle by looking up the exact object instance the load returned. Copying the result (for example with .ToList()) and releasing the copy has no effect and logs an error - always release the same instance the await produced.
To keep the loaded assets alive past the method that loaded them, see the two single-asset examples above: a CancellationTokenSource scoped to OnEnable/OnDisable for a repeatable load, or ToAwaitable(MonoBehaviour) for a one-shot load.