Async/Await and Coroutines

Introduction #

Almost everything worth doing in a game takes time: loading a level, waiting for a network response, playing an animation out to its end. Async/await and coroutines are the two dominant answers to the same problem: how do you write code that waits for something, without blocking the frame it’s running on. Unity and Unreal solve it differently, and the right choice depends on which one your engine actually offers.

Best Practices #

  • Match the tool to the wait. A fixed number of frames or a simple timer is a coroutine. A real external operation with a completion signal is async/await or a latent action.
  • Always have a cancellation path. A coroutine attached to a destroyed object, or a Task nobody’s awaiting anymore, is a leak waiting to surface as a bug report.
  • Don’t block on async code. Calling .Result or .Wait() on a Task from the main thread defeats the entire point and can deadlock.

The problem: blocking the main thread #

A game loop runs on a strict budget, usually somewhere around 16ms per frame for 60 frames a second. If a function call takes 200ms to complete and you call it directly, the game freezes for 200ms. Every asynchronous pattern exists to solve exactly this: run the slow thing, keep rendering frames, and pick back up when it’s done.

Coroutines (Unity) #

A Unity coroutine is a method that can pause its own execution with yield return and hand control back to the engine, which resumes it on a later frame.

using System.Collections;
using UnityEngine;

public class FadeExample : MonoBehaviour
{
    public void StartFade()
    {
        StartCoroutine(FadeOut(1.5f));
    }

    private IEnumerator FadeOut(float duration)
    {
        float elapsed = 0f;
        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            // adjust alpha here based on elapsed / duration
            yield return null; // resume next frame
        }
    }
}

  • Use Cases: frame-spread animation, waiting on WaitForSeconds/WaitUntil, sequencing gameplay events over time.
  • Limits: a coroutine isn’t a real thread and doesn’t return a value directly; it lives and dies with the MonoBehaviour that started it, and gets silently stopped if that object is disabled or destroyed.

Async/await (C#) and latent actions (Unreal) #

C#’s async/await (available in Unity through standard .NET, and a common pattern for tooling and UniTask-style workflows) lets a method suspend at an await point and resume once the awaited Task completes, without ever blocking the calling thread.

using System.Threading.Tasks;

public class DownloadExample
{
    public async Task LoadProfileAsync(string userId)
    {
        var data = await FetchFromServerAsync(userId);
        ApplyProfile(data);
    }

    private Task<byte[]> FetchFromServerAsync(string userId)
    {
        // real implementation would call out to a web request API
        return Task.FromResult(new byte[0]);
    }

    private void ApplyProfile(byte[] data) { }
}

Unreal’s C++ side doesn’t use async/await at all. Instead, Blueprint-exposed asynchronous work is usually built as a latent action (a UBlueprintAsyncActionBase or a latent UFUNCTION), which fires a delegate when the work completes rather than suspending a call stack.

UCLASS()
class UMyAsyncLoad : public UBlueprintAsyncActionBase
{
    GENERATED_BODY()
public:
    UPROPERTY(BlueprintAssignable)
    FLoadCompleteDelegate OnLoadComplete;

    UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
    static UMyAsyncLoad* LoadAsync(const FString& Path);

    virtual void Activate() override;
};

  • Use Cases: network calls, disk I/O, anything with a real completion signal rather than a fixed frame count.
  • Limits: exception handling across an await boundary needs care (an unobserved faulted Task fails silently in some contexts), and mixing async code with Unity’s single-threaded API (most engine calls aren’t thread-safe) needs the continuation to marshal back onto the main thread.

Rate This Article!