【Unity】Coroutines vs async/await - Which Should You Use in Unity?

Created: 2026-07-05Last updated: 2026-07-13

When you need to write code that 'waits', should you reach for a coroutine or async/await? This guide untangles one of Unity's classic dilemmas: how the two mechanisms differ, when to use which, Unity 6's Awaitable, and the destroyed-object pitfall.

"Wait 3 seconds, then explode." "Switch screens once loading finishes." The moment you try to write code that waits and search for examples, you find some written with coroutines and others with async/await — and suddenly you're stuck wondering, "So which one am I supposed to use?" It's a fork in the road every Unity developer hits at some point.

Here's the short answer up front: they aren't rivals — they're tools with different specialties. In this article we'll build an intuition for how each mechanism works, establish clear criteria for choosing between them, look at the official Awaitable type added in Unity 6, and cover async's signature pitfall: code that keeps running on destroyed objects.

Conceptual image of two asynchronous mechanisms, shown as two roads running side by side

What You'll Learn

  • How coroutines (frame-driven) and async/await (completion-driven) differ under the hood
  • Why the rule of thumb is "coroutines for effects, async for IO and networking"
  • What Unity 6's official Awaitable lets you do
  • The async pitfall: code that keeps running on destroyed objects, and destroyCancellationToken

Sponsored

Picturing the Two Mechanisms

Let's start by comparing how each one actually implements "waiting."

Comparison diagram of coroutines and async/await. A coroutine rides a train that departs every frame, advancing a little at each station, while async/await hands the work off to a separate worker and waits to be notified of completion

A coroutine rides the "frame train." Every yield return pauses the method, and it resumes on the next frame (or on whichever frame matches the condition you specified). Because it advances in lockstep with Unity's game loop, it's a natural fit for frame-by-frame effects like "fade out a little bit each frame." For a deeper look at the syntax, see the coroutines article.

async/await works like "handing a job to a worker and waiting for the callback." The rest of the method is put on hold until the await-ed operation (a file read, a network request, an asset load) finishes, then resumes. The key trait is that progress is driven by whether the work is done, completely independent of frames.

// The same "wait 1 second, then show" written both ways

// Coroutine version: ride the frame train
IEnumerator ShowAfterOneSecond()
{
    yield return new WaitForSeconds(1f);
    panel.SetActive(true);
}

// async/await version (using Unity 6's Awaitable): wait for completion
async Awaitable ShowAfterOneSecondAsync()
{
    await Awaitable.WaitForSecondsAsync(1f);
    panel.SetActive(true);
}

Looking at this example alone, they seem identical — but the gap between their strengths widens as what you're waiting for gets more complex.

When Coroutines Shine

Anything tied to in-game time and frames is coroutine territory.

Diagram of three scenarios suited to coroutines: visual effects like fades, frame-synced logic via yield, and processing that stops automatically when the object is destroyed
  • Visual effects: fade-ins, blinking, knockback, camera shake — anything in the "a little bit every frame" family
  • Frame-synced logic: waiting one frame with yield return null, or waiting for the physics step with WaitForFixedUpdate
  • Processing that should share the GameObject's fate: coroutines stop automatically when their GameObject is destroyed, so there's no cleanup to worry about

That third point is quietly important. If an enemy gets killed mid-way through its blinking effect, a coroutine simply stops on its own.

When async/await Shines

Waiting for work happening "outside the game" to finish is async/await territory.

Diagram of three scenarios suited to async/await: reading and writing save data, network communication, and asset loading — all cases of waiting for work outside the game to complete
  • File IO: saving and loading save data asynchronously
  • Network communication: fetching leaderboards, calling APIs
  • Asset loading: Addressables' LoadAssetAsync returns a handle whose Task property you can await, as in await handle.Task
  • Async operations that return a value: coroutines can't return values, but async Task<T> and async Awaitable<T> can return a result
using UnityEngine;

public class SaveService : MonoBehaviour
{
    // Returning a value is async's strength (coroutines can't do this)
    public async Awaitable<string> LoadRankingAsync()
    {
        await Awaitable.WaitForSecondsAsync(0.5f); // dummy wait standing in for a network call
        return "1st place: 12000 pts";
    }
}

"Wait on a job whose duration in frames is unknown, then take the result and continue" — the moment your code takes that shape, it's async's turn.

Sponsored

Official Support in Unity 6: Awaitable

The old refrain that "async/await in Unity takes some extra work" existed because there was no official waiting primitive equivalent to a coroutine's yield return. Unity 6 (2023.1 and later) ships the Awaitable class as standard, which has all but closed that gap.

What you want to doCoroutineasync/await (Awaitable)
Wait N secondsyield return new WaitForSeconds(1f)await Awaitable.WaitForSecondsAsync(1f)
Wait one frameyield return nullawait Awaitable.NextFrameAsync()
Wait for the physics stepyield return new WaitForFixedUpdate()await Awaitable.FixedUpdateAsync()
Wait until end of frame renderingyield return new WaitForEndOfFrame()await Awaitable.EndOfFrameAsync()

With the syntax now on equal footing, the "I want to standardize on async" camp can get there using only official APIs. That said, hold off on a wholesale migration until you've read the next section.

Caution: async Keeps Running After the Object Dies

This is where the two mechanisms differ most in personality. Coroutines stop automatically when their GameObject is destroyed — async methods do not.

Diagram of the lifetime difference between coroutines and async. When the GameObject is destroyed, the coroutine stops with it, but the async method keeps running afterward, tries to touch the vanished object, and throws an error

Even after a scene transition or an enemy's defeat removes the object, the code after await still runs and touches a destroyed component, throwing an error — the classic bug of any async migration. The fix is to pass destroyCancellationToken, which every MonoBehaviour provides out of the box.

using UnityEngine;

public class ExplosionTimer : MonoBehaviour
{
    async void Start()
    {
        // If this GameObject is destroyed, the wait is cancelled along with it
        await Awaitable.WaitForSecondsAsync(3f, destroyCancellationToken);

        Explode(); // If already destroyed, execution never reaches here
    }

    private void Explode()
    {
        Debug.Log("Boom!");
    }
}

Note: A cancelled await is interrupted via an exception (OperationCanceledException). An exception that reaches the top of an async void method is picked up by the Unity engine and logged to the Console—it doesn't just vanish quietly. If you want cancellation to count as a "normal exit," catch OperationCanceledException with a try-catch. This is exactly why async is said to be "one notch harder than coroutines."

Sponsored

Bonus: Good to Know for Later

Once you've got the basic decision framework down, these topics come into view next.

  • Solidify your coroutine fundamentals: The varieties of yield return and how to stop coroutines are covered in the coroutines article — it's the foundation this article builds on.
  • The battle-tested library: UniTask: The community-built UniTask was the backbone of async in Unity long before Awaitable arrived. With allocation-free waits and a rich operator set, it's more powerful than the official APIs in many respects — well worth a look if you're adopting async seriously.
  • Loading — where async truly shines: Asynchronous scene and asset loading is a perfect application. The LoadSceneAsync coverage in the SceneManager article and the Addressables introduction are great places to put this into practice.

Summary

Coroutines and async/await aren't a question of newer or better — they're tools with different jobs.

  • Coroutines: ride the frame train. Best for effects, frame-synced logic, and anything that should share a GameObject's lifetime. Their superpower is stopping automatically on destroy.
  • async/await: completion-driven. Best for file IO, networking, asset loading, and any operation where you need a return value.
  • Unity 6's Awaitable lets you write coroutine-style waits — seconds, frames, and more — officially in async form.
  • async keeps running after destroy. Wiring up cancellation with destroyCancellationToken is the iron rule.
  • The two can and should coexist. "Coroutines for in-game time, async for out-of-game work" is a practical starting point.

Rather than learning one and calling it a day, get in the habit of asking, "Is this wait about frames, or about a job finishing?" — and the hesitation will disappear.