The Day My Game Froze and Why Async Await Unity Saved My Project

A practical guide to asynchronous programming in Unity that keeps your game responsive, your UI smooth, and your players happy.

Here's the thing—I'll never forget the first time I tried to load a 3D scene in Unity while displaying a loading screen. I wrote what I thought was perfectly reasonable code: click the button, load the scene, show a progress bar. Simple, right? Wrong. The entire game froze. The progress bar didn't move. The music stuttered. For a solid 5 seconds, it looked like my game had crashed. That's when I learned about async await Unity the hard way—by watching my playtesters think my game was broken. Asynchronous programming with async/await solves one of the most common problems in game development: long-running operations that freeze the game. Let me show you how to avoid the painful mistake I made.

Why Your Game Probably Freezes When It Shouldn't

Been there. You're building your first real game project, and you need to load a large scene, download player data from a server, or run a complex pathfinding calculation. You write the code, hit play, and... everything locks up. The screen freezes. Input stops responding. Your beautiful game looks broken.

Actually, wait. The code is working—it's just blocking Unity's main thread. The main thread in Unity is the single, primary thread where almost all of the engine's logic must be executed, including rendering, physics updates, UI events, and script lifecycle methods like Update(). If you perform a long-running, synchronous (blocking) operation on this thread, the entire game will freeze until it is finished.

This is exactly the problem that async await Unity programming solves. It allows you to perform tasks like loading files, making web requests, or running complex calculations without blocking Unity's main thread, which is responsible for rendering frames and responding to player input. This enables you to create a smooth, responsive player experience with seamless loading screens, interactive UIs that don't lock up, and complex background processes that don't cause stuttering.

The Restaurant Buzzer Analogy That Made Everything Click

You know what's funny? The best explanation of asynchronous programming I ever heard came from my roommate at CMU, not a professor. He said: "It's like ordering food at a busy counter."

Think about it: instead of standing and waiting for your order (blocking you from doing anything else), you are given a buzzer and are free to find a table or talk to friends. The buzzer goes off when your food is ready, signaling that the task is complete and you can now proceed with eating.

That's exactly how async/await works in Unity. You start a long operation (ordering food), get a "buzzer" (a Task), and your code is free to continue doing other things (Unity keeps rendering frames and processing input). When the operation completes (buzzer goes off), your code picks up right where it left off.

Six Terms That Will Make You Sound Like a Threading Expert

Let me break down the terminology so we're all on the same page. From my time implementing async systems at KIXEYE, these six concepts are absolutely foundational:

How Async Methods Actually Work (The Signature That Changes Everything)

After working on multiple Unity projects, I've found that understanding the method signature is the first step. To create an asynchronous method, you mark it with the async keyword and typically return a Task or Task<TResult>. This signature tells the compiler that the method can be awaited.

Here's the exact structure I use:

csharp
using System.Threading.Tasks;
using UnityEngine;

public class AsyncExample : MonoBehaviour
{
    // An async method that returns no value.
    public async Task MyAsyncMethod()
    {
        Debug.Log("Starting async method...");
        await Task.Delay(1000); // Pause for 1 second without blocking.
        Debug.Log("Async method finished.");
    }
}

Notice how the method is marked with async and returns Task. The await Task.Delay(1000) line is where the magic happens—the method pauses for 1 second, but Unity keeps running. The game doesn't freeze. For more details on async operations in Unity, check out the Unity Cloud Code Docs - Async Operations.

Returning Values from Asynchronous Operations

This one had me convinced to switch from coroutines. If your asynchronous operation needs to produce a result (like data from a web request), you return a Task<T>, where T is the type of the result.

Here's a practical example I use constantly:

csharp
using System.Threading.Tasks;
using UnityEngine;

public class AsyncDataExample : MonoBehaviour
{
    // This async method will return an integer after 2 seconds.
    public async Task<int> FetchPlayerScore()
    {
        Debug.Log("Fetching score from server...");
        await Task.Delay(2000); // Simulate a network request.
        return 100; // Return the result.
    }

    private async void Start()
    {
        int score = await FetchPlayerScore();
        Debug.Log($"Player score is: {score}");
    }
}

With coroutines, returning values requires awkward callbacks or out parameters. With async/await, you just return the value. It's beautiful. GameDev Beginner has a great deep dive on this: Async/Await in Unity.

Making Unity's AsyncOperation Actually Awaitable

Here's something that tripped me up early on: many of Unity's own long-running operations, like loading a scene, return an AsyncOperation. While you can't await this object directly, you can use a simple extension method or a helper to make it awaitable, cleaning up the code significantly compared to using coroutines and callbacks.

This is the exact extension method I use in my projects:

csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;

public static class AsyncOperationExtensions
{
    // Extension method to allow awaiting of AsyncOperation
    public static Task AsTask(this AsyncOperation operation)
    {
        var tcs = new TaskCompletionSource<object>();
        operation.completed += _ => { tcs.SetResult(null); };
        return tcs.Task;
    }
}

public class SceneLoader : MonoBehaviour
{
    public async void LoadSceneAsync()
    {
        Debug.Log("Starting to load scene asynchronously...");
        // Use the extension method to await the scene loading operation.
        await SceneManager.LoadSceneAsync("MyNewScene").AsTask();
        Debug.Log("Scene loaded!");
    }
}

This pattern is discussed extensively in the Unity community. Check out this Unity Forum Discussion on Awaiting AsyncOperation for more implementation details.

Coroutines vs Async Await Unity: The Honest Comparison

When I'm working on projects, I constantly get asked: "Should I use coroutines or async/await?" Here's the honest comparison based on years of using both:

Criteria Approach A: Coroutines (IEnumerator) Approach B: async/await (Task)
Best For Simple, time-based sequences tied to a GameObject's lifecycle, like animations, cooldowns, or fading effects that need to sync with the game loop. Complex asynchronous logic, operations that need to return a value, and tasks that should not be tied to a specific GameObject's existence. Ideal for web requests and file I/O.
Performance Generally very efficient for simple frame-based waits. Can create garbage if new is used inside loops (e.g., new WaitForSeconds). Highly optimized by the C# compiler. Can be more performant for complex logic, but requires careful management to avoid running too much on the main thread.
Complexity Easy for beginners to grasp for simple waits, but becomes difficult to manage with complex branching, error handling, or when returning values (requires callbacks). The code is much cleaner and reads like synchronous code, which simplifies complex logic. Standard try-catch blocks can be used for robust error handling.
Code Example IEnumerator FadeOut() { float duration = 2f; while(duration > 0) { // fade logic... duration -= Time.deltaTime; yield return null; } } async Task FadeOutAsync() { float duration = 2f; while(duration > 0) { // fade logic... duration -= Time.deltaTime; await Task.Yield(); } }

For simple animations, coroutines are fine. But for anything involving network requests, file I/O, or complex error handling, I always reach for async/await.

Four Reasons I Never Use Blocking Code Anymore

After working on multiple Unity projects at KIXEYE and teaching hundreds of students at Outscal, here's why Unity async programming has become my default approach:

1. Your Game Actually Stays Responsive

The single most important benefit is preventing your game from freezing. By awaiting long-running tasks, the main thread is freed to continue rendering frames and processing input, resulting in a smooth and professional-feeling application. Your players will never know how much work is happening behind the scenes.

2. Your Code Becomes Actually Readable

Asynchronous code written with async/await is far easier to read and understand because it looks like normal, sequential code. This dramatically simplifies complex logic compared to the nested callbacks or complex state machines often required by coroutines. I've had junior developers pick up async code in minutes.

3. Error Handling Actually Works Like You Expect

You can wrap await calls in standard try-catch blocks, just like any synchronous code. This provides a robust and familiar way to handle exceptions from asynchronous operations, such as a failed network connection or a file not being found. No more checking boolean flags and error callbacks.

4. Returning Values Stops Being a Pain

Asynchronous methods can directly return a value using Task<T>, which is a clean and straightforward way to get the result of an operation. This is a significant improvement over coroutines, which require awkward workarounds like callbacks or out parameters.

My Three Non-Negotiable Rules for Async Programming

Trust me, you'll thank me later for these tips. I learned all of these the hard way—some through crashes, some through performance issues, and one through a very angry tech lead at KIXEYE.

Rule #1: Avoid async void Like the Plague (Except for Event Handlers)

Methods returning async void are "fire and forget" and cannot be awaited, making it impossible to know when they've completed. More importantly, any exception thrown in an async void method will crash the application. Always prefer async Task unless you are writing an event handler (e.g., for a UI button click).

Here's the exact pattern I follow:

csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncBestPractice : MonoBehaviour
{
    public Button myButton;

    void Start()
    {
        // GOOD: Using async Task allows the caller to await the operation.
        myButton.onClick.AddListener(async () => await HandleClickAsync());
    }

    // BAD: 'async void' should be avoided. This is only acceptable because it's an event handler.
    private async void OnButtonClick()
    {
        Debug.Log("Button clicked!");
        await Task.Delay(1000);
        Debug.Log("Action complete.");
    }

    // GOOD: This method returns a Task, so it can be awaited and exceptions can be caught.
    public async Task HandleClickAsync()
    {
        try
        {
            Debug.Log("Handling click...");
            await Task.Delay(1000); // Simulate work
            Debug.Log("Click handled successfully.");
        }
        catch (System.Exception e)
        {
            Debug.LogError($"An error occurred: {e.Message}");
        }
    }
}

Microsoft has excellent guidance on this: Async/Await Best Practices.

Rule #2: Use CancellationToken for Long-Running Operations

If you start an operation that might need to be stopped (e.g., the player closes a menu while a file is downloading), you should use a CancellationToken. This provides a reliable way to signal cancellation to the asynchronous method so it can stop gracefully.

This is the exact implementation I use:

csharp
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class CancellationExample : MonoBehaviour
{
    private CancellationTokenSource _cancellationSource;

    public async void StartLongOperation()
    {
        _cancellationSource = new CancellationTokenSource();
        try
        {
            Debug.Log("Starting operation...");
            await DoWorkAsync(_cancellationSource.Token);
            Debug.Log("Operation completed.");
        }
        catch (TaskCanceledException)
        {
            Debug.Log("Operation was canceled.");
        }
    }

    public void CancelOperation()
    {
        _cancellationSource?.Cancel();
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        for (int i = 0; i < 10; i++)
        {
            token.ThrowIfCancellationRequested(); // Check for cancellation.
            Debug.Log($"Work step {i + 1}...");
            await Task.Delay(500, token);
        }
    }

    void OnDestroy()
    {
        // Ensure the task is cancelled when the object is destroyed.
        CancelOperation();
    }
}

The official documentation on this is essential reading: Microsoft Docs - Task Cancellation.

Rule #3: Keep Unity API Calls on the Main Thread

Most of Unity's API is not thread-safe and must be called from the main thread. If you use Task.Run to perform work on a background thread, you must switch back to the main thread to update GameObjects, UI, etc. await Task.Yield() is a simple way to wait for the next frame on the main thread.

Here's my go-to pattern for background work:

csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class ThreadingExample : MonoBehaviour
{
    public Text statusText;

    public async void ProcessDataOnBackgroundThread()
    {
        statusText.text = "Processing...";

        // Run the complex calculation on a background thread to avoid freezing.
        string result = await Task.Run(() =>
        {
            // This part is NOT on the main thread.
            // Do not call Unity APIs here.
            System.Threading.Thread.Sleep(3000); // Simulate heavy work.
            return "Data Processed!";
        });

        // await Task.Yield(); // This is one way to get back to the main thread.
        // In modern Unity, the context is usually captured automatically.

        // This part IS back on the main thread.
        // It is now safe to call Unity APIs.
        statusText.text = result;
    }
}

Unity has introduced new features to make this easier. Read about them here: Unity Blog - Awaitable.

How Stardew Valley, Hearthstone, and Minecraft Handle Long Operations

Let me tell you about how I've seen this technique used brilliantly in some of my favorite games. After analyzing these implementations, I realized the patterns we've been discussing aren't theoretical—they're how professional studios ship smooth, responsive games.

The Stardew Valley Saving System That Taught Me Everything

One of my favorite implementations of asynchronous operations is in Stardew Valley. At the end of each day, the game saves your entire farm's state, inventory, and relationships. This is a significant amount of data that needs to be written to a file.

From a developer's perspective, what makes this brilliant is the implementation: this process is likely handled by an async method. When the player goes to bed, an async Task could be called to serialize all game data into JSON or another format and then write it to the disk asynchronously. While this happens, the UI can show a "Saving..." animation without the music stuttering or the screen freezing.

What I find fascinating about this approach is the player experience it creates: the player experiences a smooth, seamless transition to the next day. The saving process feels like a natural part of the game's flow rather than an intrusive, performance-hindering interruption.

Here's simplified pseudo-code demonstrating the concept:

csharp
// Simplified pseudo-code demonstrating the concept.
public class GameManager : MonoBehaviour
{
    public async Task SaveGameAsync()
    {
        // Show "Saving..." UI
        UIManager.ShowSavingIndicator(true);

        // This part runs without blocking the main thread.
        await Task.Run(() =>
        {
            string gameData = SerializeGameState();
            System.IO.File.WriteAllText("save.json", gameData);
        });

        // Hide UI now that the task is complete.
        UIManager.ShowSavingIndicator(false);
    }
}

How Hearthstone Keeps Its UI Responsive

This one really sold me on async programming. In Hearthstone, when you open your card collection or the game starts, it needs to fetch your player data, card library, and daily quests from a remote server.

After analyzing this implementation, async/await is perfect for this. An async Task using UnityWebRequest or HttpClient would be called to make the API request. The game can await the result while showing loading spinners and animations, ensuring the UI remains alive and responsive. The returned JSON data is then parsed and used to populate the UI.

Here's how you can adapt this for your own game: the player never sees a completely frozen screen. They are always given visual feedback that the game is working, which makes the wait feel shorter and the application feel more polished and professional.

csharp
// Simplified pseudo-code demonstrating the concept.
public class CollectionManager : MonoBehaviour
{
    public async Task<CardCollection> FetchCollectionFromServer()
    {
        using (var request = new UnityEngine.Networking.UnityWebRequest("https://my-game-api.com/collection"))
        {
            request.downloadHandler = new UnityEngine.Networking.DownloadHandlerBuffer();
            await request.SendWebRequest().AsTask(); // Using the extension method

            if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
            {
                string json = request.downloadHandler.text;
                return JsonUtility.FromJson<CardCollection>(json);
            }
            return null;
        }
    }
}

The Minecraft Chunk Loading System That Blew My Mind

Let me tell you about the mechanic that inspired my current approach to world streaming: in Minecraft, as the player moves through the world, the game must constantly load new terrain chunks and unload old ones to create the illusion of an infinite world.

What I find fascinating about this approach is that this is a prime candidate for asynchronous work. An async method could be responsible for loading chunk data from a file, generating the mesh, and preparing the GameObjects. This heavy lifting can be done with Task.Run on a background thread. Once a chunk is ready, the final step of instantiating it into the scene is done back on the main thread.

This is why I always recommend studying this game's approach: the player can explore a vast, seamless world without ever hitting an invisible wall or a loading screen. The world appears to generate just in front of them, providing a smooth and immersive exploration experience.

csharp
// Simplified pseudo-code demonstrating the concept.
public class WorldStreaming : MonoBehaviour
{
    public async Task LoadChunkAsync(Vector2Int chunkCoord)
    {
        // Heavy data processing on a background thread.
        ChunkData data = await Task.Run(() =>
        {
            // Load from file and generate mesh data
            return new ChunkData(); // Placeholder
        });

        // Instantiation must happen on the main thread.
        InstantiateChunkInScene(data);
    }
}

Blueprint #1: Scene Loading with a Smooth Progress Bar

Let's tackle this together and build the exact scene loading system that would have saved me from that embarrassing freeze I mentioned at the beginning. Our goal: to load a new game scene in the background after a button click, while smoothly updating a UI slider to show the loading progress, preventing the game from freezing.

Unity Editor Setup

Here's my go-to setup process:

  1. Create a new Scene named "MyGameScene" (or any name)
  2. In your starting scene, create a UI Canvas
  3. Inside the Canvas, add a UI Button and a UI Slider
  4. Create an empty GameObject named "SceneLoader" and attach the script below to it
  5. Assign the Button and Slider to the script's fields in the Inspector

Step-by-Step Implementation

Let me show you how I approach this, step by step.

Step 1: Script Setup

Create a new C# script named AsyncSceneLoader and add the necessary using statements for UI, Scene Management, and Tasks:

csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class AsyncSceneLoader : MonoBehaviour
{
    public Button loadSceneButton;
    public Slider progressBar;
    public string sceneToLoad = "MyGameScene";
}

Step 2: Button Event Listener

In the Start method, add a listener to the button's onClick event. We use an async lambda to allow the use of await inside the listener:

csharp
// Add this inside the AsyncSceneLoader class
void Start()
{
    // Disable the progress bar initially
    progressBar.gameObject.SetActive(false);

    // Add a listener to the button to call our async method
    loadSceneButton.onClick.AddListener(async () =>
    {
        await LoadSceneWithProgressAsync();
    });
}

Step 3: Core Async Loading Logic

Create the async Task method that handles the scene loading. It will activate the progress bar, start the LoadSceneAsync operation, and then loop until the scene is loaded, updating the slider's value each frame:

csharp
// Add this inside the AsyncSceneLoader class
private async Task LoadSceneWithProgressAsync()
{
    // Make the button non-interactable and show the progress bar
    loadSceneButton.interactable = false;
    progressBar.gameObject.SetActive(true);

    // Start loading the scene asynchronously.
    AsyncOperation sceneLoadOperation = SceneManager.LoadSceneAsync(sceneToLoad);
    sceneLoadOperation.allowSceneActivation = false; // Don't activate the scene immediately

    // Loop until the scene is almost fully loaded
    while (sceneLoadOperation.progress < 0.9f)
    {
        // Update the progress bar
        progressBar.value = sceneLoadOperation.progress;

        // Wait until the next frame to continue the loop
        await Task.Yield();
    }

    // The scene is loaded, now update the bar to full and allow activation
    progressBar.value = 1f;
    sceneLoadOperation.allowSceneActivation = true;
}

This is the exact code I use for scene loading in production. The await Task.Yield() is crucial—it tells Unity to continue this loop on the next frame, keeping the UI responsive.

Blueprint #2: Fetching Data from a Web API Without Freezing

When I'm working on online games, my process always includes robust API integration. Let me walk you through creating a system to download simple JSON data from a public API when a button is clicked and display the result in a UI Text element, without freezing the UI during the request.

Unity Editor Setup

  1. In your scene, create a UI Canvas
  2. Inside the Canvas, add a UI Button and a UI Text element
  3. Create an empty GameObject named "ApiManager" and attach the script below
  4. Assign the Button and Text to the script's fields in the Inspector

Step-by-Step Implementation

Step 1: Script Setup and Data Structure

Create a script named ApiManager. We'll use JsonPlaceholder as a sample API. First, define a simple class to hold the deserialized JSON data:

csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;

// A simple class to match the JSON structure from the API
[System.Serializable]
public class TodoItem
{
    public int userId;
    public int id;
    public string title;
    public bool completed;
}

public class ApiManager : MonoBehaviour
{
    public Button fetchDataButton;
    public Text resultText;
    private const string ApiUrl = "https://jsonplaceholder.typicode.com/todos/1";
}

Step 2: Button Event Listener

In Start, hook up the button to call our async data fetching method:

csharp
// Add this inside the ApiManager class
void Start()
{
    fetchDataButton.onClick.AddListener(async () =>
    {
        resultText.text = "Fetching data...";
        TodoItem item = await FetchTodoItemAsync();
        if (item != null)
        {
            resultText.text = $"Title: {item.title}\nCompleted: {item.completed}";
        }
        else
        {
            resultText.text = "Failed to fetch data.";
        }
    });
}

This pattern demonstrates how to handle async operations triggered by UI events while providing user feedback throughout the process.

Ready to Start Building Your First Game?

If you've made it this far, you understand one of the most critical skills in modern game development: writing responsive, non-blocking code that creates smooth player experiences. The async await Unity patterns we've covered—Task-based programming, proper thread management, and practical async operations—are exactly the kind of foundational knowledge that separates amateur projects from professional games.

At Outscal, I've built a complete game development course that takes you from the absolute basics to building professional-quality experiences. We cover not just asynchronous programming, but all the optimization techniques, architectural patterns, and practical skills you need to break into the game industry.

Whether you're working on your first Unity project or trying to level up from hobbyist to professional, the Mr. Blocks course will give you the exact roadmap I wish I had when I was starting out. No fluff, no outdated techniques—just the real-world skills studios are looking for.


Key Takeaways

Common Questions

What is async await Unity and why should I use it?+

Async await Unity refers to using C#'s async/await keywords for asynchronous programming in Unity. You should use it because it prevents your game from freezing during long-running operations like loading scenes, making web requests, or performing complex calculations. Instead of blocking the main thread (which handles rendering and input), async/await allows these operations to run while keeping your game responsive and smooth.

What is the difference between async and await in C#?+

The async keyword is a modifier you add to a method signature to mark it as asynchronous, enabling the use of await inside it. The await keyword is an operator you use inside async methods to pause execution until a Task completes, without blocking the thread. Think of async as declaring "this method can pause" and await as the actual pause point.

When should I use coroutines vs async await in Unity?+

Use coroutines for simple, frame-based sequences tied to a GameObject's lifecycle, like animations, cooldowns, or fading effects. Use async/await for complex asynchronous logic, operations that need to return values, web requests, file I/O, and tasks that shouldn't be tied to a specific GameObject's existence. Async/await provides cleaner code, better error handling with try-catch, and easy value returning.

What is a Task in C# Unity?+

A Task is an object that represents an asynchronous operation that will eventually complete in the future. You can await a Task to wait for its completion without blocking the thread. Task is used for operations that don't return a value, while Task<T> returns a value of type T (like Task<int> for an integer result).

How do I make Unity's AsyncOperation awaitable?+

Unity's AsyncOperation (used for scene loading and asset loading) isn't directly awaitable, but you can create a simple extension method using TaskCompletionSource. The extension method subscribes to the completed event and signals the Task when done, allowing you to use await SceneManager.LoadSceneAsync("Scene").AsTask().

What does async void do and why should I avoid it?+

async void methods are "fire and forget"—they can't be awaited and you can't know when they complete. More critically, any exception thrown in an async void method will crash your application because there's no way to catch it. Always use async Task instead, except for UI event handlers where async void is acceptable.

How do I cancel an async operation in Unity?+

Use a CancellationToken passed to your async method. Create a CancellationTokenSource, pass its .Token to your method, and check token.ThrowIfCancellationRequested() periodically in your async code. Call _cancellationSource.Cancel() when you need to stop the operation. Always cancel tokens in OnDestroy() to prevent tasks from running after objects are destroyed.

Can I call Unity APIs from a background thread?+

No! Most of Unity's API is not thread-safe and must be called from the main thread. If you use Task.Run() to perform work on a background thread, you must switch back to the main thread before calling Unity APIs. Use await Task.Yield() to return to the main thread, or rely on Unity's automatic context capture in modern versions.

What is the Unity main thread and why does it matter for async programming?+

The Unity main thread (or UI thread) is the single primary thread where almost all of Unity's engine logic executes—rendering, physics updates, UI events, and script lifecycle methods like Update(). If you perform long-running blocking operations on this thread, your entire game freezes. Async programming keeps the main thread responsive by spreading work across frames or moving heavy work to background threads.

How do I handle errors in async methods?+

Wrap await calls in standard try-catch blocks, just like synchronous code. This is one of the major advantages of async/await over coroutines. You can catch specific exceptions (like TaskCanceledException for cancelled operations) and handle network failures, file errors, or other issues gracefully with familiar error-handling patterns.

What is Task.Delay vs Task.Yield in Unity?+

Task.Delay(milliseconds) pauses execution for a specified time without blocking the thread—it's like a non-blocking wait. Task.Yield() yields control back to the calling context and continues on the next frame, similar to yield return null in coroutines. Use Task.Yield() to break up long operations across multiple frames in Unity.

How do games like Stardew Valley and Minecraft use async programming?+

Stardew Valley likely uses async methods for save operations, serializing game data and writing to disk on a background thread while showing a "Saving..." animation without freezing. Minecraft uses async operations to load world chunks on background threads—loading data and generating meshes asynchronously, then instantiating them on the main thread. This creates smooth, seamless gameplay without loading screens or stuttering.