Error Handling
Proper error handling patterns
Master advanced error handling patterns with NOPE-PRO. This tutorial covers everything from basic error handling to complex recovery strategies.
Understanding Error Types
String Errors (Quick & Simple)
Best for prototyping and simple cases:
1public Result<Player, string> LoadPlayer(string id)
2{
3 if (string.IsNullOrEmpty(id))
4 return "Invalid player ID";
5
6 if (!File.Exists($"saves/{id}.json"))
7 return "Player save not found";
8
9 // ... load player
10 return player;
11}
Pros: Quick to write, easy to understand
Cons: No structure, hard to handle programmatically
Enum Errors (Structured)
Better for production code with known error cases:
1public enum PlayerError
2{
3 InvalidId,
4 NotFound,
5 CorruptedData,
6 VersionMismatch,
7 PermissionDenied
8}
9
10public Result<Player, PlayerError> LoadPlayer(string id)
11{
12 if (string.IsNullOrEmpty(id))
13 return PlayerError.InvalidId;
14
15 if (!File.Exists($"saves/{id}.json"))
16 return PlayerError.NotFound;
17
18 // ... more checks
19 return player;
20}
21
22// Handle specific errors
23LoadPlayer("player123")
24 .Match(
25 onSuccess: player =>
26 {
27 InitializePlayer(player);
28 return Unit.Value;
29 },
30 onFailure: error =>
31 {
32 switch (error)
33 {
34 case PlayerError.NotFound:
35 CreateNewPlayer();
36 break;
37 case PlayerError.VersionMismatch:
38 MigratePlayerData();
39 break;
40 default:
41 ShowErrorDialog(error.ToString());
42 break;
43 }
44 return Unit.Value;
45 }
46 );
Custom Error Classes (Most Flexible)
Best for complex applications:
1// Base error class
2public abstract class GameError
3{
4 public string Message { get; }
5 public DateTime Timestamp { get; }
6
7 protected GameError(string message)
8 {
9 Message = message;
10 Timestamp = DateTime.Now;
11 }
12}
13
14// Specific error types
15public class ValidationError : GameError
16{
17 public string FieldName { get; }
18 public object AttemptedValue { get; }
19
20 public ValidationError(string fieldName, object attemptedValue, string message)
21 : base(message)
22 {
23 FieldName = fieldName;
24 AttemptedValue = attemptedValue;
25 }
26}
27
28public class NetworkError : GameError
29{
30 public int StatusCode { get; }
31 public string Endpoint { get; }
32
33 public NetworkError(int statusCode, string endpoint, string message)
34 : base(message)
35 {
36 StatusCode = statusCode;
37 Endpoint = endpoint;
38 }
39}
40
41// Usage
42public Result<Player, GameError> ValidatePlayer(Player player)
43{
44 if (player.Level < 1 || player.Level > 100)
45 return new ValidationError("Level", player.Level,
46 "Level must be between 1 and 100");
47
48 if (string.IsNullOrEmpty(player.Name))
49 return new ValidationError("Name", null,
50 "Player name is required");
51
52 return player;
53}
Error Recovery Patterns
Pattern 1: Fallback Values
1public class ConfigLoader
2{
3 public GameConfig LoadConfig()
4 {
5 return LoadFromFile()
6 .OrElse(LoadFromPlayerPrefs)
7 .OrElse(LoadFromResources)
8 .Match(
9 config => config,
10 errorMessage =>
11 {
12 Debug.LogError($"Failed to load game config: {errorMessage}");
13 return GetDefaultConfig();
14 }
15 );
16 }
17
18 private Result<GameConfig, string> LoadFromFile()
19 {
20 return Result.Of(
21 () => JsonUtility.FromJson<GameConfig>(File.ReadAllText("config.json")),
22 ex => "File not found or invalid"
23 );
24 }
25
26 private Result<GameConfig, string> LoadFromPlayerPrefs()
27 {
28 var json = PlayerPrefs.GetString("GameConfig", "");
29 return string.IsNullOrEmpty(json)
30 ? "No config in PlayerPrefs"
31 : JsonUtility.FromJson<GameConfig>(json);
32 }
33
34 private Result<GameConfig, string> LoadFromResources()
35 {
36 var configAsset = Resources.Load<TextAsset>("DefaultConfig");
37 return configAsset != null
38 ? JsonUtility.FromJson<GameConfig>(configAsset.text)
39 : "No default config in Resources";
40 }
41
42 private GameConfig GetDefaultConfig()
43 {
44 return new GameConfig
45 {
46 Difficulty = Difficulty.Normal,
47 SoundVolume = 0.8f,
48 MusicVolume = 0.5f
49 };
50 }
51}
Pattern 2: Retry with Backoff
1public class NetworkService
2{
3 public async UniTask<Result<T, NetworkError>> RequestWithRetry<T>(
4 string endpoint,
5 int maxRetries = 3,
6 float baseDelay = 1f)
7 {
8 Result<T, NetworkError> result = default;
9
10 for (int attempt = 0; attempt < maxRetries; attempt++)
11 {
12 result = await MakeRequest<T>(endpoint);
13
14 if (result.IsSuccess)
15 return result;
16
17 // Check if error is retryable
18 if (!IsRetryable(result.Error))
19 return result;
20
21 // Exponential backoff
22 if (attempt < maxRetries - 1)
23 {
24 var delay = baseDelay * Mathf.Pow(2, attempt);
25 Debug.Log($"Retry {attempt + 1}/{maxRetries} after {delay}s");
26 await UniTask.Delay(TimeSpan.FromSeconds(delay));
27 }
28 }
29
30 return result.MapError(e => new NetworkError(
31 e.StatusCode,
32 e.Endpoint,
33 $"{e.Message} (after {maxRetries} attempts)"
34 ));
35 }
36
37 private bool IsRetryable(NetworkError error)
38 {
39 // Retry on timeout or server errors
40 return error.StatusCode >= 500 || error.StatusCode == 408;
41 }
42}
Pattern 3: Graceful Degradation
1public class PlayerDataService
2{
3 public Result<PlayerStats, string> LoadPlayerStats(string playerId)
4 {
5 return LoadFullStats(playerId)
6 .EnableDebug("LoadStats")
7 .MapError(error =>
8 {
9 Debug.LogWarning($"Full stats failed: {error}");
10 return error;
11 })
12 .OrElse(() => LoadCachedStats(playerId)
13 .Map(stats =>
14 {
15 stats.IsFromCache = true;
16 return stats;
17 }))
18 .OrElse(() => CreateMinimalStats(playerId));
19 }
20
21 private Result<PlayerStats, string> LoadFullStats(string playerId)
22 {
23 // Load from server
24 return Result<PlayerStats, string>.Failure("Server unavailable");
25 }
26
27 private Result<PlayerStats, string> LoadCachedStats(string playerId)
28 {
29 var cached = PlayerPrefs.GetString($"stats_{playerId}", "");
30 if (string.IsNullOrEmpty(cached))
31 return "No cached stats";
32
33 return Result.Of(
34 () => JsonUtility.FromJson<PlayerStats>(cached),
35 ex => "Cached stats corrupted"
36 );
37 }
38
39 private Result<PlayerStats, string> CreateMinimalStats(string playerId)
40 {
41 Debug.Log("Creating minimal stats for offline play");
42 return new PlayerStats
43 {
44 PlayerId = playerId,
45 Level = 1,
46 Experience = 0,
47 IsFromCache = true,
48 IsMinimal = true
49 };
50 }
51}
Pattern 4: Error Aggregation
1public class BatchProcessor
2{
3 public class BatchResult
4 {
5 public List<Item> SuccessfulItems { get; } = new();
6 public List<(Item item, string error)> FailedItems { get; } = new();
7
8 public bool HasErrors => FailedItems.Count > 0;
9 public float SuccessRate => SuccessfulItems.Count /
10 (float)(SuccessfulItems.Count + FailedItems.Count);
11 }
12
13 public BatchResult ProcessItems(List<Item> items)
14 {
15 var result = new BatchResult();
16
17 foreach (var item in items)
18 {
19 ProcessItem(item)
20 .EnableDebug($"Process_{item.id}")
21 .Match<Unit>(
22 onSuccess: processedItem =>
23 {
24 result.SuccessfulItems.Add(processedItem);
25 return Unit.Value;
26 },
27 onFailure: error =>
28 {
29 result.FailedItems.Add((item, error));
30 Debug.LogWarning($"Failed to process {item.id}: {error}");
31
32 return Unit.Value;
33 }
34 );
35 }
36
37 Debug.Log($"Batch complete: {result.SuccessRate:P} success rate");
38 return result;
39 }
40
41 private Result<Item, string> ProcessItem(Item item)
42 {
43 return ValidateItem(item)
44 .Bind(TransformItem)
45 .Bind(SaveItem);
46 }
47}
Advanced Error Handling
Contextual Errors
Add context as errors propagate up:
1public class GameSaveService
2{
3 public Result<Unit, string> SaveGame(string slot)
4 {
5 return GatherSaveData()
6 .MapError(e => $"Failed to gather data: {e}")
7 .Bind(data => SerializeData(data)
8 .MapError(e => $"Serialization failed: {e}"))
9 .Bind(json => WriteToFile(slot, json)
10 .MapError(e => $"File write failed: {e}"))
11 .Tap(_ => Debug.Log($"Game saved to slot {slot}"))
12 .MapError(e => $"Save operation failed in slot {slot}: {e}");
13 }
14}
Error Transformation Pipeline
1public class ErrorTransformer
2{
3 // Convert low-level errors to user-friendly messages
4 public static string ToUserFriendly(GameError error)
5 {
6 return error switch
7 {
8 NetworkError net when net.StatusCode == 404 =>
9 "The requested content was not found.",
10 NetworkError net when net.StatusCode >= 500 =>
11 "Server is experiencing issues. Please try again later.",
12 ValidationError val =>
13 $"Invalid {val.FieldName}: {val.Message}",
14 _ => "An unexpected error occurred. Please try again."
15 };
16 }
17
18 // Usage
19 public void ShowError(Result<Unit, GameError> result)
20 {
21 result
22 .MapError(ToUserFriendly)
23 .Match(
24 onSuccess: _ => Unit.Value,
25 onFailure: userMessage => { ShowErrorDialog(userMessage); return Unit.Value; }
26 );
27 }
28}
Compensating Actions
Undo operations when something fails:
1public class TransactionManager
2{
3 private Stack<Action> compensations = new();
4
5 public Result<Unit, string> ExecuteTransaction(List<Operation> operations)
6 {
7 compensations.Clear();
8
9 foreach (var op in operations)
10 {
11 var result = op.Execute()
12 .Tap(_ => compensations.Push(op.Compensate));
13
14 if (result.IsFailure)
15 {
16 // Rollback all successful operations
17 Rollback();
18 return $"Transaction failed at {op.Name}: {result.Error}";
19 }
20 }
21
22 return Unit.Value;
23 }
24
25 private void Rollback()
26 {
27 Debug.Log($"Rolling back {compensations.Count} operations");
28
29 while (compensations.Count > 0)
30 {
31 try
32 {
33 compensations.Pop().Invoke();
34 }
35 catch (Exception ex)
36 {
37 Debug.LogError($"Compensation failed: {ex.Message}");
38 }
39 }
40 }
41}
Error Handling Best Practices
1. Be Specific with Error Messages
1// Bad - vague error
2if (player.Gold < itemPrice)
3 return "Insufficient resources";
4
5// Good - specific and actionable
6if (player.Gold < itemPrice)
7 return $"Not enough gold. Need {itemPrice}, have {player.Gold}";
2. Use Error Types for Different Handling
1public interface IRecoverableError
2{
3 Result<Unit, IGameError> Recover();
4}
5
6public class CacheExpiredError : GameError, IRecoverableError
7{
8 private readonly Func<Result<Unit, IGameError>> refreshCache;
9
10 public CacheExpiredError(Func<Result<Unit, IGameError>> refreshCache)
11 : base("Cache has expired")
12 {
13 this.refreshCache = refreshCache;
14 }
15
16 public Result<Unit, IGameError> Recover()
17 {
18 Debug.Log("Attempting to refresh cache...");
19 return refreshCache();
20 }
21}
22
23// Usage
24result.Match(
25 onSuccess: data => { HandleSuccess(data); return Unit.Value; },
26 onFailure: error =>
27 {
28 if (error is IRecoverableError recoverable)
29 {
30 recoverable.Recover()
31 .Match(
32 onSuccess: _ => { RetryOriginalOperation(); return Unit.Value; },
33 onFailure: e => { ShowFatalError(e); return Unit.Value; }
34 );
35 }
36 else
37 {
38 ShowError(error);
39 }
40 }
41);
3. Log Errors Appropriately
1public static class ResultLogging
2{
3 public static Result<T, E> LogError<T, E>(this Result<T, E> result, string context)
4 {
5 if (result.IsFailure)
6 {
7 var error = result.Error;
8
9 // Log based on error severity
10 if (error is CriticalError)
11 Debug.LogError($"[CRITICAL] {context}: {error}");
12 else if (error is Warning)
13 Debug.LogWarning($"[WARNING] {context}: {error}");
14 else
15 Debug.Log($"[INFO] {context}: {error}");
16
17 // Send to analytics if needed
18 Analytics.LogError(context, error.ToString());
19 }
20
21 return result;
22 }
23}
4. Create Error Helpers
1public static class ErrorHelpers
2{
3 // Validate multiple conditions
4 public static Result<T, string> Validate<T>(
5 T value,
6 params (Func<T, bool> predicate, string error)[] validations)
7 {
8 foreach (var (predicate, error) in validations)
9 {
10 if (!predicate(value))
11 return error;
12 }
13 return value;
14 }
15
16 // Usage
17 public Result<Player, string> ValidatePlayer(Player player)
18 {
19 return ErrorHelpers.Validate(player,
20 (p => !string.IsNullOrEmpty(p.Name), "Name is required"),
21 (p => p.Level >= 1, "Level must be at least 1"),
22 (p => p.Level <= 100, "Level cannot exceed 100"),
23 (p => p.Health > 0, "Player must have health")
24 );
25 }
26}
Complete Example: Robust Save System
Here's a complete example showing all error handling patterns:
1using UnityEngine;
2using System;
3using System.Collections.Generic;
4using NOPE.Runtime.Core;
5using NOPE.Runtime.Core.Result;
6using NOPE.PRO.VisualDebugger;
7
8public class RobustSaveSystem : MonoBehaviour
9{
10 // Error types
11 public enum SaveError
12 {
13 InvalidData,
14 SerializationFailed,
15 DiskFull,
16 PermissionDenied,
17 CorruptedFile,
18 NetworkError
19 }
20
21 // Save data structure
22 [Serializable]
23 public class SaveData
24 {
25 public string playerId;
26 public int version;
27 public DateTime timestamp;
28 public Dictionary<string, object> gameState;
29 }
30
31 private const int CURRENT_VERSION = 2;
32 private const int MAX_RETRIES = 3;
33
34 // Main save method with all error handling
35 public void SaveGame(string slot)
36 {
37 CreateSaveData()
38 .EnableDebug($"SaveGame_{slot}")
39 .Bind(data => ValidateSaveData(data))
40 .Bind(data => SerializeWithBackup(data))
41 .Bind(json => SaveWithRetry(slot, json, MAX_RETRIES))
42 .Tap(_ => OnSaveSuccess(slot))
43 .Match(
44 onSuccess: _ => { ShowSaveSuccessUI(slot); return Unit.Value; },
45 onFailure: error => { HandleSaveError(error, slot); return Unit.Value; }
46 );
47 }
48
49 // Create save data with error handling
50 private Result<SaveData, SaveError> CreateSaveData()
51 {
52 try
53 {
54 var data = new SaveData
55 {
56 playerId = GetPlayerId(),
57 version = CURRENT_VERSION,
58 timestamp = DateTime.Now,
59 gameState = GatherGameState()
60 };
61
62 return data;
63 }
64 catch (Exception ex)
65 {
66 Debug.LogError($"Failed to create save data: {ex}");
67 return SaveError.InvalidData;
68 }
69 }
70
71 // Validate with specific checks
72 private Result<SaveData, SaveError> ValidateSaveData(SaveData data)
73 {
74 if (string.IsNullOrEmpty(data.playerId))
75 return SaveError.InvalidData;
76
77 if (data.gameState == null || data.gameState.Count == 0)
78 return SaveError.InvalidData;
79
80 return data;
81 }
82
83 // Serialize with backup of previous data
84 private Result<string, SaveError> SerializeWithBackup(SaveData data)
85 {
86 return Result.Of(
87 () => {
88 // Backup existing data first
89 BackupExistingData();
90
91 // Serialize new data
92 return JsonUtility.ToJson(data, true);
93 },
94 ex => {
95 Debug.LogError($"Serialization failed: {ex}");
96 return SaveError.SerializationFailed;
97 }
98 );
99 }
100
101 // Save with retry logic
102 private Result<Unit, SaveError> SaveWithRetry(string slot, string json, int retriesLeft)
103 {
104 var result = AttemptSave(slot, json);
105
106 if (result.IsSuccess || retriesLeft <= 0)
107 return result;
108
109 // Only retry certain errors
110 if (IsRetryableError(result.Error))
111 {
112 Debug.Log($"Save failed, retrying... ({retriesLeft} attempts left)");
113 System.Threading.Thread.Sleep(1000); // Wait before retry
114 return SaveWithRetry(slot, json, retriesLeft - 1);
115 }
116
117 return result;
118 }
119
120 // Actual save operation
121 private Result<Unit, SaveError> AttemptSave(string slot, string json)
122 {
123 try
124 {
125 var path = GetSavePath(slot);
126
127 // Check disk space
128 if (GetAvailableDiskSpace() < json.Length * 2)
129 return SaveError.DiskFull;
130
131 // Write to file
132 System.IO.File.WriteAllText(path, json);
133
134 // Verify write
135 var verification = System.IO.File.ReadAllText(path);
136 if (verification != json)
137 return SaveError.CorruptedFile;
138
139 return Unit.Value;
140 }
141 catch (UnauthorizedAccessException)
142 {
143 return SaveError.PermissionDenied;
144 }
145 catch (Exception ex)
146 {
147 Debug.LogError($"Save failed: {ex}");
148 return SaveError.CorruptedFile;
149 }
150 }
151
152 // Error handling
153 private void HandleSaveError(SaveError error, string slot)
154 {
155 var recovery = error switch
156 {
157 SaveError.DiskFull => RecoverFromDiskFull(),
158 SaveError.PermissionDenied => RecoverFromPermissionDenied(),
159 SaveError.NetworkError => OfferOfflineSave(slot),
160 _ => Result<Unit, SaveError>.Failure(error)
161 };
162
163 recovery.Match(
164 onSuccess: _ => {
165 Debug.Log("Recovery successful, retrying save...");
166 SaveGame(slot); // Retry after recovery
167 return Unit.Value;
168 },
169 onFailure: e => {
170 var userMessage = GetUserFriendlyMessage(e);
171 ShowErrorDialog(userMessage);
172
173 // Restore from backup if critical
174 if (IsCriticalError(e))
175 RestoreFromBackup();
176
177 return Unit.Value;
178 }
179 );
180 }
181
182 // Recovery strategies
183 private Result<Unit, SaveError> RecoverFromDiskFull()
184 {
185 Debug.Log("Attempting to clear cache for space...");
186
187 return Result.Of<Unit, SaveError>(
188 () => {
189 ClearCache();
190 DeleteOldSaves();
191 return Unit.Value;
192 },
193 ex => SaveError.DiskFull
194 );
195 }
196
197 // Helper methods
198 private bool IsRetryableError(SaveError error)
199 {
200 return error == SaveError.NetworkError ||
201 error == SaveError.CorruptedFile;
202 }
203
204 private bool IsCriticalError(SaveError error)
205 {
206 return error == SaveError.CorruptedFile ||
207 error == SaveError.InvalidData;
208 }
209
210 private string GetUserFriendlyMessage(SaveError error)
211 {
212 return error switch
213 {
214 SaveError.DiskFull => "Not enough storage space. Please free up some space and try again.",
215 SaveError.PermissionDenied => "Cannot save to this location. Please check permissions.",
216 SaveError.NetworkError => "Connection lost. Your progress will be saved locally.",
217 SaveError.CorruptedFile => "Save file corrupted. Restored from backup.",
218 _ => "Failed to save game. Please try again."
219 };
220 }
221
222 // Stub implementations
223 private string GetPlayerId() => "player123";
224 private Dictionary<string, object> GatherGameState() => new();
225 private void BackupExistingData() { }
226 private long GetAvailableDiskSpace() => 1000000;
227 private string GetSavePath(string slot) => $"saves/{slot}.json";
228 private void OnSaveSuccess(string slot) { }
229 private void ShowSaveSuccessUI(string slot) { }
230 private void ShowErrorDialog(string message) => Debug.LogError(message);
231 private Result<Unit, SaveError> RecoverFromPermissionDenied() => SaveError.PermissionDenied;
232 private Result<Unit, SaveError> OfferOfflineSave(string slot) => Unit.Value;
233 private void ClearCache() { }
234 private void DeleteOldSaves() { }
235 private void RestoreFromBackup() { }
236}
Summary
You've learned:
- ✅ Different error type strategies (string, enum, custom classes)
- ✅ Error recovery patterns (fallback, retry, degradation)
- ✅ Advanced error handling techniques
- ✅ Best practices for production code
- ✅ How to build robust systems with comprehensive error handling