Migration Guide - From Traditional to NOPE-PRO
This guide helps you migrate existing try-catch based code to NOPE-PRO's Railway-Oriented Programming approach.
Why Migrate?
Traditional Problems
1// Traditional approach - problems hidden everywhere
2public Player LoadPlayer(string id)
3{
4 try
5 {
6 var data = Database.Load(id); // Could return null
7 var json = Decrypt(data); // Could throw
8 var player = Parse(json); // Could throw
9
10 if (player == null) // Null check
11 throw new Exception("Null player");
12
13 if (!Validate(player)) // Validation
14 return null; // Inconsistent error handling
15
16 return player;
17 }
18 catch (DatabaseException) // Multiple catch blocks
19 {
20 Debug.LogError("DB error");
21 return null;
22 }
23 catch (CryptoException)
24 {
25 Debug.LogError("Decrypt error");
26 return CreateDefaultPlayer(); // Different error handling
27 }
28 catch (Exception e)
29 {
30 Debug.LogError($"Unknown: {e}");
31 throw; // Re-throwing?
32 }
33}Problems:
- Mixed error handling (null, exceptions, default values)
- Hidden failure points
- Inconsistent error responses
- Difficult to test
- Hard to maintain
NOPE-PRO Solution
1// NOPE-PRO approach - explicit and clean
2public Result<Player, string> LoadPlayer(string id)
3{
4 return Result.Of(
5 () => Database.Load(id),
6 ex => "Player not found"
7 )
8 .Bind(data => Decrypt(data))
9 .Bind(json => Parse(json))
10 .Ensure(player => Validate(player), "Invalid player data")
11 .MapError(error => $"Failed to load player {id}: {error}");
12}Benefits:
- Explicit error handling
- Consistent error types
- Easy to test
- Self-documenting
- Composable
Migration Patterns
Pattern 1: Simple Try-Catch
Before
1public Config LoadConfig()
2{
3 try
4 {
5 string json = File.ReadAllText("config.json");
6 return JsonUtility.FromJson<Config>(json);
7 }
8 catch (Exception e)
9 {
10 Debug.LogError($"Config error: {e.Message}");
11 return new Config(); // Default
12 }
13}After
1public Result<Config, string> LoadConfig()
2{
3 return Result.Of(
4 () => File.ReadAllText("config.json"),
5 ex => $"Failed to read config: {ex.Message}"
6 )
7 .Bind(json => Result.Of(
8 () => JsonUtility.FromJson<Config>(json),
9 ex => $"Invalid config format: {ex.Message}"
10 ));
11}
12
13// Usage with fallback
14public Config GetConfigOrDefault()
15{
16 return LoadConfig()
17 .Or(new Config()) // Provide default on failure
18 .Match(
19 onSuccess: config => config,
20 onFailure: error => {
21 Debug.LogError(error);
22 return new Config();
23 }
24 );
25}Pattern 2: Null Checks
Before
1public string GetUserDisplayName(string userId)
2{
3 var user = GetUser(userId);
4 if (user == null)
5 return "Unknown User";
6
7 var profile = user.Profile;
8 if (profile == null)
9 return user.Username;
10
11 var displayName = profile.DisplayName;
12 if (string.IsNullOrEmpty(displayName))
13 return user.Username;
14
15 return displayName;
16}After
1public string GetUserDisplayName(string userId)
2{
3 var user = GetUser(userId);
4 if (user == null)
5 return "Unknown User";
6
7 var profile = user.Profile;
8 if (profile == null)
9 return user.Username;
10
11 return string.IsNullOrEmpty(profile.DisplayName)
12 ? user.Username
13 : profile.DisplayName;
14}
15
16// With Result - cleaner approach
17public Result<string, string> GetUserDisplayNameSafe(string userId)
18{
19 return GetUserResult(userId)
20 .Map(user => user.Profile?.DisplayName ?? user.Username)
21 .Ensure(name => !string.IsNullOrEmpty(name), "No display name available");
22}
23
24// With Maybe - if using Maybe<User>
25public string GetUserDisplayNameWithMaybe(string userId)
26{
27 return GetMaybeUser(userId)
28 .Map(user => user.Profile?.DisplayName ?? user.Username)
29 .Where(name => !string.IsNullOrEmpty(name))
30 .Or("Unknown User");
31}Pattern 3: Validation Chains
Before
1public bool ProcessOrder(Order order)
2{
3 if (order == null)
4 {
5 LogError("Order is null");
6 return false;
7 }
8
9 if (order.Items.Count == 0)
10 {
11 LogError("No items in order");
12 return false;
13 }
14
15 if (order.Customer == null)
16 {
17 LogError("No customer");
18 return false;
19 }
20
21 if (order.Total <= 0)
22 {
23 LogError("Invalid total");
24 return false;
25 }
26
27 try
28 {
29 ChargePayment(order);
30 ShipOrder(order);
31 return true;
32 }
33 catch (PaymentException e)
34 {
35 LogError($"Payment failed: {e}");
36 return false;
37 }
38 catch (ShippingException e)
39 {
40 LogError($"Shipping failed: {e}");
41 RefundPayment(order);
42 return false;
43 }
44}After
1public Result<Order, string> ProcessOrder(Order order)
2{
3 return ValidateOrder(order)
4 .Bind(ChargePayment)
5 .Bind(ShipOrder)
6 .MapError(error => {
7 if (error.Contains("Payment"))
8 RefundPayment(order);
9 return error;
10 });
11}
12
13private Result<Order, string> ValidateOrder(Order order)
14{
15 if (order == null)
16 return "Order is null";
17
18 return Result<Order, string>.Success(order)
19 .Ensure(o => o.Items.Any(), "No items in order")
20 .Ensure(o => o.Customer != null, "No customer")
21 .Ensure(o => o.Total > 0, "Invalid total");
22}
23
24private Result<Order, string> ChargePayment(Order order)
25{
26 return Result.Of(
27 () => {
28 PaymentService.Charge(order);
29 return order;
30 },
31 ex => $"Payment failed: {ex.Message}"
32 );
33}Pattern 4: Resource Management
Before
1public Data ProcessFile(string path)
2{
3 FileStream stream = null;
4 try
5 {
6 stream = File.OpenRead(path);
7 var data = ReadData(stream);
8 var processed = Process(data);
9 return processed;
10 }
11 catch (FileNotFoundException)
12 {
13 return null;
14 }
15 catch (IOException e)
16 {
17 LogError($"IO error: {e}");
18 return null;
19 }
20 finally
21 {
22 stream?.Dispose();
23 }
24}After
1public Result<Data, string> ProcessFile(string path)
2{
3 return Result.Of(() =>
4 {
5 using (var stream = File.OpenRead(path))
6 {
7 return ReadData(stream);
8 }
9 }, ex => ex switch
10 {
11 FileNotFoundException => $"File not found: {path}",
12 IOException ioEx => $"IO error: {ioEx.Message}",
13 _ => $"Unexpected error: {ex.Message}"
14 })
15 .Bind(data => ProcessData(data));
16}Pattern 5: Async Operations
Before
1public async Task<User> GetUserAsync(string id)
2{
3 try
4 {
5 var response = await httpClient.GetAsync($"api/users/{id}");
6 if (!response.IsSuccessStatusCode)
7 {
8 if (response.StatusCode == HttpStatusCode.NotFound)
9 return null;
10 throw new Exception($"HTTP {response.StatusCode}");
11 }
12
13 var json = await response.Content.ReadAsStringAsync();
14 return JsonConvert.DeserializeObject<User>(json);
15 }
16 catch (HttpRequestException e)
17 {
18 LogError($"Network error: {e}");
19 return null;
20 }
21 catch (TaskCanceledException)
22 {
23 LogError("Request timeout");
24 return null;
25 }
26}After
1#if NOPE_UNITASK
2public async UniTask<Result<User, string>> GetUserAsync(string id)
3{
4 return await Result.OfAsync(async () =>
5 {
6 var response = await httpClient.GetAsync($"api/users/{id}");
7 response.EnsureSuccessStatusCode();
8 return await response.Content.ReadAsStringAsync();
9 }, ex => ex switch
10 {
11 HttpRequestException => "Network error",
12 TaskCanceledException => "Request timeout",
13 _ => $"HTTP error: {ex.Message}"
14 })
15 .Bind(json => ParseUser(json));
16}
17#endifStep-by-Step Migration Process
Step 1: Identify Error Patterns
Look for:
try-catchblocksnullchecks- Boolean return values indicating success
- Throwing exceptions
- Multiple return types (null, default, throw)
Step 2: Choose Error Type
1// Simple cases - use string
2Result<Data, string> LoadData();
3
4// Known errors - use enum
5public enum DataError { NotFound, Invalid, Unauthorized }
6Result<Data, DataError> LoadData();
7
8// Complex cases - use custom type
9public class DataError
10{
11 public string Message { get; }
12 public ErrorCode Code { get; }
13 public Dictionary<string, object> Context { get; }
14}
15Result<Data, DataError> LoadData();Step 3: Convert Method Signatures
1// Before
2public Player LoadPlayer(string id);
3public Task<bool> SaveAsync(Data data);
4public Config GetConfig();
5
6// After
7public Result<Player, string> LoadPlayer(string id);
8public async UniTask<Result<Unit, string>> SaveAsync(Data data);
9public Maybe<Config> GetConfig();Step 4: Replace Error Handling
1// Before
2try
3{
4 var result = DoSomething();
5 return result;
6}
7catch (Exception e)
8{
9 HandleError(e);
10 return default;
11}
12
13// After
14return DoSomething()
15 .MapError(error => {
16 HandleError(error);
17 return error;
18 });Step 5: Update Callers
1// Before
2var player = LoadPlayer(id);
3if (player == null)
4{
5 ShowError("Failed to load");
6 return;
7}
8UsePlayer(player);
9
10// After
11LoadPlayer(id)
12 .Match(
13 onSuccess: player => { UsePlayer(player); return Unit.Value; },
14 onFailure: error => { ShowError($"Failed: {error}"); return Unit.Value; }
15 );Common Pitfalls
1. Overusing Try-Catch Inside Result
1// Bad - defeats the purpose
2public Result<Data, string> LoadData()
3{
4 try
5 {
6 return DoSomething();
7 }
8 catch (Exception e)
9 {
10 return e.Message;
11 }
12}
13
14// Good - use Result.Of
15public Result<Data, string> LoadData()
16{
17 return Result.Of(
18 () => DoSomething(),
19 ex => ex.Message
20 );
21}2. Ignoring Error Context
1// Bad - generic error
2return Result<Data, string>.Failure("Error");
3
4// Good - specific error with context
5return Result<Data, string>.Failure($"Failed to load data for user {userId}: {specificError}");3. Not Handling All Paths
1// Bad - ignoring failure
2var result = LoadData();
3if (result.IsSuccess)
4 Process(result.Value);
5// What about failure?
6
7// Good - handle both paths
8LoadData().Match(
9 onSuccess: data => { Process(data); return Unit.Value; },
10 onFailure: error => { LogError(error); return Unit.Value; }
11);Migration Checklist
- Add NOPE-PRO to project
- Enable
NOPE_PRO_DEBUGfor development - Identify methods to migrate
- Choose appropriate error types
- Convert method signatures
- Replace try-catch with Result.Of
- Replace null checks with Maybe
- Update all callers
- Add visual debugging
- Test error scenarios
- Remove old error handling code
Real-World Migration Example
Here's a complete before/after of a save system:
Before (Traditional)
1public class SaveManager : MonoBehaviour
2{
3 public bool SaveGame(SaveData data)
4 {
5 if (data == null)
6 {
7 Debug.LogError("Save data is null");
8 return false;
9 }
10
11 try
12 {
13 // Validate
14 if (!data.IsValid())
15 throw new InvalidOperationException("Invalid save data");
16
17 // Serialize
18 string json = JsonUtility.ToJson(data);
19 if (string.IsNullOrEmpty(json))
20 throw new Exception("Serialization failed");
21
22 // Compress
23 byte[] compressed = Compress(json);
24
25 // Save locally
26 File.WriteAllBytes(GetSavePath(), compressed);
27
28 // Upload to cloud
29 if (IsOnline())
30 {
31 try
32 {
33 UploadToCloud(compressed);
34 }
35 catch (Exception cloudEx)
36 {
37 Debug.LogWarning($"Cloud save failed: {cloudEx}");
38 // Continue anyway
39 }
40 }
41
42 Debug.Log("Save successful");
43 return true;
44 }
45 catch (Exception e)
46 {
47 Debug.LogError($"Save failed: {e}");
48
49 // Try to restore backup
50 try
51 {
52 RestoreBackup();
53 }
54 catch
55 {
56 // Give up
57 }
58
59 return false;
60 }
61 }
62}After (NOPE-PRO)
1public class SaveManager : MonoBehaviour
2{
3 public Result<SaveResult, SaveError> SaveGame(SaveData data)
4 {
5 return ValidateSaveData(data)
6 .EnableDebug("SaveGame")
7 .Bind(Serialize)
8 .Bind(Compress)
9 .Bind(SaveLocally)
10 .Tap(result => AttemptCloudSave(result))
11 .Match(
12 onSuccess: result => {
13 Debug.Log("Save successful");
14 return Result<SaveResult, SaveError>.Success(result);
15 },
16 onFailure: error => {
17 Debug.LogError($"Save failed: {error}");
18 return RestoreBackup()
19 .MapError(backupError =>
20 new SaveError(SaveErrorType.Critical,
21 $"Save failed and backup failed: {error}, {backupError}"));
22 }
23 );
24 }
25
26 private Result<SaveData, SaveError> ValidateSaveData(SaveData data)
27 {
28 if (data == null)
29 return new SaveError(SaveErrorType.InvalidData, "Save data is null");
30
31 if (!data.IsValid())
32 return new SaveError(SaveErrorType.InvalidData, "Invalid save data");
33
34 return data;
35 }
36
37 private Result<string, SaveError> Serialize(SaveData data)
38 {
39 return Result.Of(
40 () => JsonUtility.ToJson(data),
41 ex => new SaveError(SaveErrorType.SerializationFailed, ex.Message)
42 )
43 .Ensure(json => !string.IsNullOrEmpty(json),
44 new SaveError(SaveErrorType.SerializationFailed, "Empty JSON"));
45 }
46
47 private Result<byte[], SaveError> Compress(string json)
48 {
49 return Result.Of(
50 () => CompressionUtil.Compress(json),
51 ex => new SaveError(SaveErrorType.CompressionFailed, ex.Message)
52 );
53 }
54
55 private Result<SaveResult, SaveError> SaveLocally(byte[] data)
56 {
57 return Result.Of(
58 () => {
59 var path = GetSavePath();
60 File.WriteAllBytes(path, data);
61 return new SaveResult
62 {
63 LocalPath = path,
64 Size = data.Length,
65 Timestamp = DateTime.Now
66 };
67 },
68 ex => new SaveError(SaveErrorType.IOError, $"Local save failed: {ex.Message}")
69 );
70 }
71
72 private void AttemptCloudSave(SaveResult result)
73 {
74 if (!IsOnline()) return;
75
76 UploadToCloud(result)
77 .Match(
78 onSuccess: _ => { Debug.Log("Cloud save successful"); return Unit.Value; },
79 onFailure: error => { Debug.LogWarning($"Cloud save failed: {error}"); return Unit.Value; }
80 );
81 }
82}
83
84public class SaveError
85{
86 public SaveErrorType Type { get; }
87 public string Message { get; }
88
89 public SaveError(SaveErrorType type, string message)
90 {
91 Type = type;
92 Message = message;
93 }
94
95 public override string ToString() => $"[{Type}] {Message}";
96}
97
98public enum SaveErrorType
99{
100 InvalidData,
101 SerializationFailed,
102 CompressionFailed,
103 IOError,
104 CloudError,
105 Critical
106}Summary
Migration benefits:
- ✅ Explicit error handling
- ✅ Better testability
- ✅ Cleaner code
- ✅ Visual debugging
- ✅ Consistent patterns