Async Operations Tutorial

Working with UniTask and Awaitable

Learn how to use NOPE-PRO with asynchronous operations using UniTask or Unity's built-in Awaitable system.

Prerequisites

Before starting, ensure you have:

  • Enabled either NOPE_UNITASK or NOPE_AWAITABLE in NOPE-PRO Settings Panel in Window -> NOPE-PRO -> Settings or Project Settings
  • For UniTask: Installed the UniTask package
  • For Awaitable: Using Unity 6 or newer

Choosing Your Async System

1// In Project Settings → Player → Scripting Define Symbols
2NOPE_UNITASK
3
4// Your code
5using Cysharp.Threading.Tasks;
6
7public async UniTask<Result<Data, string>> LoadDataAsync() { }

Awaitable (Unity 6+)

1// In Project Settings → Player → Scripting Define Symbols
2NOPE_AWAITABLE
3
4// Your code
5using UnityEngine;
6
7public async Awaitable<Result<Data, string>> LoadDataAsync() { }

Basic Async Usage

Converting Sync to Async

1// Synchronous version
2public Result<GameData, string> LoadGameData(string filename)
3{
4    return Result.Of(
5            () => File.ReadAllText(filename),
6            ex => $"Failed to read file: {ex.Message}"
7        )
8        .Bind(json => ParseJson<GameData>(json));
9}
10
11// Async version with UniTask
12#if NOPE_UNITASK
13public async UniTask<Result<GameData, string>> LoadGameDataAsync(string filename)
14{
15    return await Result.Of(
16        async () => await ReadAllTextAsync(filename),
17        ex => $"Failed to read file: {ex.Message}"
18    )
19    .Bind(json => ParseJsonAsync<GameData>(json));
20}
21
22private async UniTask<string> ReadAllTextAsync(string filename)
23{
24    return await File.ReadAllTextAsync(filename);
25}
26#endif
27
28// Async version with Awaitable
29#if NOPE_AWAITABLE
30public async Awaitable<Result<GameData, string>> LoadGameDataAsync(string filename)
31{
32    return await Result.Of(
33            async () => await ReadAllTextAsync(filename),
34            ex => $"Failed to read file: {ex.Message}"
35        )
36        .Bind(json => ParseJson<GameData>(json));
37}
38
39private async Awaitable<string> ReadAllTextAsync(string filename)
40{
41    return await File.ReadAllTextAsync(filename);
42}
43#endif

Async Result Creation

1#if NOPE_UNITASK
2// From async operation
3public async UniTask<Result<User, string>> GetUserAsync(string userId)
4{
5    try
6    {
7        var response = await UnityWebRequest.Get($"api/users/{userId}")
8            .SendWebRequest()
9            .ToUniTask();
10            
11        if (response.result != UnityWebRequest.Result.Success)
12            return $"Network error: {response.error}";
13            
14        var user = JsonUtility.FromJson<User>(response.downloadHandler.text);
15        return user;
16    }
17    catch (Exception ex)
18    {
19        return $"Unexpected error: {ex.Message}";
20    }
21}
22
23// Using Result.Of with async
24public async UniTask<Result<User, string>> GetUserSafeAsync(string userId)
25{
26    return await Result.Of(
27        async () => {
28            var response = await UnityWebRequest.Get($"api/users/{userId}")
29                .SendWebRequest()
30                .ToUniTask();
31                
32            if (response.result != UnityWebRequest.Result.Success)
33                throw new Exception(response.error);
34                
35            return JsonUtility.FromJson<User>(response.downloadHandler.text);
36        },
37        ex => $"Failed to get user: {ex.Message}"
38    );
39}
40#endif

Async Chaining Operations

Map and Bind with Async

1public class GameService
2{
3    // Mix sync and async operations
4    public async UniTask<Result<GameSession, string>> StartGameSession(string playerId)
5    {
6        return await ValidatePlayerId(playerId)              // Sync: Result<string, string>
7            .Bind(id => LoadPlayerAsync(id))            // Async: UniTask<Result<Player, string>>
8            .Map(player => CreateSessionAsync(player))  // Async: UniTask<GameSession>
9            .Ensure(session => ValidateSessionAsync(session), 
10                "Session validation failed")         // Async validation
11            .Tap(session => LogSessionStartAsync(session)); // Async side effect
12    }
13
14    // Sync validation
15    private Result<string, string> ValidatePlayerId(string playerId)
16    {
17        if (string.IsNullOrEmpty(playerId))
18            return Result<string, string>.Failure("Invalid player ID");
19        return Result<string, string>.Success(playerId);
20    }
21
22    // Async player loading
23    private async UniTask<Result<Player, string>> LoadPlayerAsync(string playerId)
24    {
25        await UniTask.Delay(100); // Simulate network delay
26    
27        // Load from database/API
28        return new Player { Id = playerId, Name = "Player1" };
29    }
30
31    // Async session creation
32    private async UniTask<GameSession> CreateSessionAsync(Player player)
33    {
34        await UniTask.Delay(50);
35    
36        return new GameSession
37        {
38            SessionId = Guid.NewGuid().ToString(),
39            Player = player,
40            StartTime = DateTime.Now
41        };
42    }
43
44    // Async validation
45    private async UniTask<bool> ValidateSessionAsync(GameSession session)
46    {
47        await UniTask.Delay(10);
48        return session.Player != null && !string.IsNullOrEmpty(session.SessionId);
49    }
50
51    // Async logging
52    private async UniTask LogSessionStartAsync(GameSession session)
53    {
54        await UniTask.Delay(5);
55        Debug.Log($"Session {session.SessionId} started for {session.Player.Name}");
56    }
57}
58#endif

Async Combination Patterns

1#if NOPE_UNITASK
2public class DataLoader
3{
4    // Parallel loading with error handling
5    public async UniTask<Result<GameData, string>> LoadAllGameDataAsync()
6    {
7        // Start all tasks in parallel
8        var configTask = LoadConfigAsync();
9        var assetsTask = LoadAssetsAsync();
10        var saveDataTask = LoadSaveDataAsync();
11    
12        // Wait for all to complete
13        var results = await UniTask.WhenAll(configTask, assetsTask, saveDataTask);
14    
15        // Combine results
16        return Result.CombineValues(results.Item1, results.Item2, results.Item3)
17            .Map(tuple => new GameData
18            {
19                Config = tuple.Item1,
20                Assets = tuple.Item2,
21                SaveData = tuple.Item3
22            });
23    }
24
25    // Sequential loading with dependencies
26    public async UniTask<Result<GameState, string>> InitializeGameAsync()
27    {
28        return await LoadConfigAsync()
29            .Bind(config => LoadAssetsForConfig(config)
30                .Map(assets => (config, assets)))
31            .Bind(tuple => LoadSaveDataWithAssets(tuple.assets)
32                .Map(saveData => new GameState
33                {
34                    Config = tuple.config,
35                    Assets = tuple.assets,
36                    SaveData = saveData
37                }))
38            .EnableDebug("GameInitialization");
39    }
40}
41#endif

Error Handling in Async Operations

Cancellation Support

1#if NOPE_UNITASK
2public class CancellableOperations
3{
4    private CancellationTokenSource cts;
5    
6    public async UniTask<Result<LevelData, string>> LoadLevelAsync(string levelId)
7    {
8        cts?.Cancel();
9        cts = new CancellationTokenSource();
10        
11        try
12        {
13            return await LoadLevelInternalAsync(levelId, cts.Token)
14                .EnableDebug($"LoadLevel_{levelId}");
15        }
16        catch (OperationCanceledException)
17        {
18            return "Level loading was cancelled";
19        }
20    }
21    
22    private async UniTask<Result<LevelData, string>> LoadLevelInternalAsync(
23        string levelId,
24        CancellationToken cancellationToken)
25    {
26        // Check cancellation before each step
27        cancellationToken.ThrowIfCancellationRequested();
28        
29        var manifest = await LoadManifestAsync(levelId);
30        if (manifest.IsFailure)
31            return manifest.Error;
32            
33        cancellationToken.ThrowIfCancellationRequested();
34        
35        var assets = await LoadAssetsAsync(manifest.Value, cancellationToken);
36        if (assets.IsFailure)
37            return assets.Error;
38            
39        cancellationToken.ThrowIfCancellationRequested();
40        
41        return new LevelData
42        {
43            Id = levelId,
44            Manifest = manifest.Value,
45            Assets = assets.Value
46        };
47    }
48    
49    public void CancelLoading()
50    {
51        cts?.Cancel();
52    }
53}
54#endif

Retry with Async

1#if NOPE_UNITASK
2public static class AsyncRetryExtensions
3{
4    public static async UniTask<Result<T, E>> RetryAsync<T, E>(
5        this Func<UniTask<Result<T, E>>> operation,
6        int maxAttempts,
7        float delaySeconds,
8        Func<E, bool> shouldRetry = null)
9    {
10        Result<T, E> result = default;
11        
12        for (int attempt = 1; attempt <= maxAttempts; attempt++)
13        {
14            result = await operation();
15            
16            if (result.IsSuccess)
17                return result;
18                
19            if (shouldRetry != null && !shouldRetry(result.Error))
20                return result;
21                
22            if (attempt < maxAttempts)
23            {
24                Debug.Log($"Attempt {attempt} failed, retrying in {delaySeconds}s...");
25                await UniTask.Delay(TimeSpan.FromSeconds(delaySeconds));
26            }
27        }
28        
29        return result;
30    }
31}
32
33// Usage
34public async UniTask<Result<ServerData, string>> FetchDataWithRetry()
35{
36    return await AsyncRetryExtensions.RetryAsync(
37        operation: FetchFromServerAsync,
38        maxAttempts: 3,
39        delaySeconds: 1.0f,
40        shouldRetry: error => !error.Contains("401") // Don't retry auth errors
41    );
42}
43#endif

Real-World Examples

Example 1: Save System with Cloud Sync

1#if NOPE_UNITASK
2public class CloudSaveManager : MonoBehaviour
3{
4    private bool isOnline = true;
5
6    public async UniTask SaveGameAsync(SaveData data)
7    {
8        await SerializeSaveData(data)
9            .EnableDebug("SaveGame")
10            .Bind(json => SaveLocallyAsync(json))
11            .Bind(json => isOnline
12                ? SyncToCloudAsync(json)
13                : UniTask.FromResult(Result<string, string>.Success(json)))
14            .Match(
15                onSuccess: _ => { ShowSaveSuccessUI(); return Unit.Value; },
16                onFailure: error => { ShowSaveErrorUI(error); return Unit.Value; }
17            );
18    }
19    
20    private Result<string, string> SerializeSaveData(SaveData data)
21    {
22        return Result.Of(
23            () => JsonUtility.ToJson(data),
24            ex => $"Serialization failed: {ex.Message}"
25        );
26    }
27
28    private async UniTask<Result<string, string>> SaveLocallyAsync(string json)
29    {
30        try
31        {
32            var path = Path.Combine(Application.persistentDataPath, "save.json");
33            await File.WriteAllTextAsync(path, json);
34            return Result<string, string>.Success(json);
35        }
36        catch (Exception ex)
37        {
38            return Result<string, string>.Failure($"Local save failed: {ex.Message}");
39        }
40    }
41
42    private async UniTask<Result<string, string>> SyncToCloudAsync(string json)
43    {
44        return await AsyncRetryExtensions.RetryAsync(
45            operation: () => UploadToCloudAsync(json),
46            maxAttempts: 3,
47            delaySeconds: 2.0f
48        );
49    }
50    
51    private void ShowSaveSuccessUI() => Debug.Log("Save successful!");
52    private void ShowSaveErrorUI(string error) => Debug.LogError($"Save failed: {error}");
53    private UniTask<Result<string, string>> UploadToCloudAsync(string json)
54    {
55        Debug.Log("Uploading to cloud...");
56        return UniTask.FromResult(Result<string, string>.Success(json));
57    }
58}
59#endif

Example 2: Resource Loading Pipeline

1#if NOPE_UNITASK
2public class ResourceManager : MonoBehaviour
3{
4    private readonly Dictionary<string, object> cache = new();
5    
6    public async UniTask<Result<T, string>> LoadResourceAsync<T>(string path) 
7        where T : UnityEngine.Object
8    {
9        return await CheckCache<T>(path)
10            .OrElse(() => LoadFromStreamingAssets<T>(path))
11            .OrElse(() => LoadFromResources<T>(path))
12            .OrElse(() => DownloadFromServer<T>(path))
13            .Tap(resource => CacheResource(path, resource))
14            .EnableDebug($"LoadResource_{path}");
15    }
16    
17    private Result<T, string> CheckCache<T>(string path) where T : UnityEngine.Object
18    {
19        if (cache.TryGetValue(path, out var cached) && cached is T typedCache)
20        {
21            Debug.Log($"Loaded {path} from cache");
22            return typedCache;
23        }
24        return "Not in cache";
25    }
26    
27    private async UniTask<Result<T, string>> LoadFromStreamingAssets<T>(string path)
28        where T : UnityEngine.Object
29    {
30        var fullPath = Path.Combine(Application.streamingAssetsPath, path);
31
32        if (!File.Exists(fullPath))
33            return "Not in streaming assets";
34
35        try
36        {
37            var assetBundle = await AssetBundle.LoadFromFileAsync(fullPath);
38            if (assetBundle == null)
39            {
40                return "Failed to load AssetBundle.";
41            }
42            
43            var assetRequest = assetBundle.LoadAssetAsync<T>(Path.GetFileNameWithoutExtension(path));
44            await assetRequest;
45            var loadedAsset = assetRequest.asset;
46
47            assetBundle.Unload(false);
48
49            if (loadedAsset == null)
50            {
51                return "Asset not found in bundle";
52            }
53            
54            return Result<T, string>.Success((T)loadedAsset);
55        }
56        catch (Exception ex)
57        {
58            return $"Streaming assets load failed: {ex.Message}";
59        }
60    }
61    
62    private async UniTask<Result<T, string>> LoadFromResources<T>(string path) 
63        where T : UnityEngine.Object
64    {
65        var request = Resources.LoadAsync<T>(path);
66        await request.ToUniTask();
67        
68        return request.asset != null
69            ? Result<T, string>.Success((T)request.asset)
70            : "Not in Resources";
71    }
72    
73    private async UniTask<Result<T, string>> DownloadFromServer<T>(string path) 
74        where T : UnityEngine.Object
75    {
76        // Implementation depends on your server setup
77        await UniTask.Delay(1000);
78        return "Server download not implemented";
79    }
80    
81    private async UniTask CacheResource<T>(string path, T resource)
82    {
83        cache[path] = resource;
84        await UniTask.Yield(); // Ensure we don't block
85    }
86}
87#endif

Example 3: Complex Game Flow

1#if NOPE_UNITASK
2public class GameFlowController : MonoBehaviour
3{
4    public async UniTask StartNewGameAsync(string playerName)
5    {
6        var flow = await CreateNewPlayer(playerName)
7            .EnableDebug("NewGameFlow")
8            .Bind(player => SelectStartingClass(player))
9            .Bind(player => AllocateStartingResources(player))
10            .Bind(player => GenerateStartingWorld(player))
11            .Tap(player => SavePlayerProgressAsync(player))
12            .Bind(player => LoadFirstLevel(player))
13            .Finally(async result => {
14                if (result.IsFailure)
15                {
16                    await ShowErrorAndReturnToMenuAsync(result.Error);
17                }
18                return result;
19            });
20    }
21    
22    private Result<Player, string> CreateNewPlayer(string name)
23    {
24        if (string.IsNullOrWhiteSpace(name))
25            return "Player name cannot be empty";
26            
27        if (name.Length < 3 || name.Length > 20)
28            return "Player name must be 3-20 characters";
29            
30        return new Player
31        {
32            Id = Guid.NewGuid().ToString(),
33            Name = name,
34            Level = 1,
35            Experience = 0
36        };
37    }
38    
39    private async UniTask<Result<Player, string>> SelectStartingClass(Player player)
40    {
41        var classSelection = await ShowClassSelectionUIAsync();
42        
43        if (classSelection.IsCancelled)
44            return "Player cancelled class selection";
45            
46        player.Class = classSelection.SelectedClass;
47        player.Stats = GetStartingStatsForClass(classSelection.SelectedClass);
48        
49        return player;
50    }
51    
52    private async UniTask<Result<Player, string>> AllocateStartingResources(Player player)
53    {
54        await UniTask.Delay(500); // Simulate allocation
55        
56        player.Inventory = new Inventory
57        {
58            Gold = 100,
59            Items = GetStartingItemsForClass(player.Class)
60        };
61        
62        return player;
63    }
64    
65    private async UniTask<Result<Player, string>> GenerateStartingWorld(Player player)
66    {
67        try
68        {
69            var worldGen = new WorldGenerator();
70            player.WorldSeed = UnityEngine.Random.Range(0, int.MaxValue);
71            
72            await worldGen.GenerateAsync(player.WorldSeed)
73                .WithTimeout(30.0f);
74                
75            return player;
76        }
77        catch (TimeoutException)
78        {
79            return "World generation timed out";
80        }
81        catch (Exception ex)
82        {
83            return $"World generation failed: {ex.Message}";
84        }
85    }
86    
87    private async UniTask<Result<Player, string>> LoadFirstLevel(Player player)
88    {
89        var levelId = GetStartingLevelForClass(player.Class);
90        
91        return await LoadLevelAsync(levelId)
92            .Map(_ => {
93                SpawnPlayer(player);
94                ShowTutorial(player.Class);
95                return player;
96            });
97    }
98}
99#endif

Performance Considerations

Avoid Unnecessary Allocations

1#if NOPE_UNITASK
2// Bad - creates new UniTask for sync operation
3public async UniTask<Result<int, string>> CalculateAsync(int x, int y)
4{
5    return await UniTask.FromResult(Result<int, string>.Success(x + y));
6}
7
8// Good - return sync result directly
9public Result<int, string> Calculate(int x, int y)
10{
11    return x + y;
12}
13
14// Good - only use async when needed
15public async UniTask<Result<Data, string>> ProcessAsync(string input)
16{
17    var parsed = ParseInput(input); // Sync operation
18    if (parsed.IsFailure)
19        return parsed.Error;
20        
21    return await FetchDataAsync(parsed.Value); // Actual async operation
22}
23#endif

Batch Async Operations

1#if NOPE_UNITASK
2public class BatchProcessor
3{
4    // Process items in parallel batches
5    public async UniTask<Result<List<ProcessedItem>, string>> ProcessItemsAsync(
6        List<Item> items,
7        int batchSize = 10)
8    {
9        var results = new List<ProcessedItem>();
10        var errors = new List<string>();
11    
12        for (int i = 0; i < items.Count; i += batchSize)
13        {
14            var batch = items.Skip(i).Take(batchSize);
15            var batchTasks = batch.Select(item => ProcessItemAsync(item));
16        
17            var batchResults = await UniTask.WhenAll(batchTasks);
18        
19            foreach (var result in batchResults)
20            {
21                result.Match(
22                    onSuccess: item => { results.Add(item); return Unit.Value; },
23                    onFailure: error => { errors.Add(error); return Unit.Value; }
24                );
25            }
26        }
27    
28        return errors.Any()
29            ? $"Failed to process {errors.Count} items: {string.Join(", ", errors.Take(3))}"
30            : results;
31    }
32    
33    private async UniTask<Result<ProcessedItem, string>> ProcessItemAsync(Item item)
34    {
35        try
36        {
37            // Simulate processing
38            await UniTask.Delay(100);
39            return new ProcessedItem { Name = item.Name, Value = item.Value };
40        }
41        catch (Exception ex)
42        {
43            return $"Error processing item {item.Name}: {ex.Message}";
44        }
45    }
46}
47#endif

Testing Async Code

1#if NOPE_UNITASK && UNITY_INCLUDE_TESTS
2using NUnit.Framework;
3using Cysharp.Threading.Tasks;
4
5public class AsyncResultTests
6{
7    [Test]
8    public async UniTask TestAsyncChaining()
9    {
10        var result = await LoadDataAsync()
11            .Bind(data => ProcessDataAsync(data))
12            .Map(processed => processed.ToString());
13            
14        Assert.IsTrue(result.IsSuccess);
15        Assert.AreEqual("Expected output", result.Value);
16    }
17    
18    [Test]
19    public async UniTask TestAsyncErrorHandling()
20    {
21        var result = await FailingOperationAsync()
22            .MapError(error => $"Handled: {error}")
23            .OrElse(() => FallbackOperationAsync());
24            
25        Assert.IsTrue(result.IsSuccess);
26    }
27    
28    [Test]
29    public async UniTask TestAsyncTimeout()
30    {
31        var result = await LongRunningOperationAsync()
32            .WithTimeout(0.1f);
33            
34        Assert.IsTrue(result.IsFailure);
35        Assert.IsTrue(result.Error.Contains("timed out"));
36    }
37}
38#endif

Summary

You've learned:

  • ✅ How to enable and use async support in NOPE-PRO
  • ✅ Converting between sync and async operations
  • ✅ Chaining async operations with proper error handling
  • ✅ Implementing timeout and cancellation
  • ✅ Building real-world async systems
  • ✅ Performance best practices