Skip to content

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#endif

Step-by-Step Migration Process

Step 1: Identify Error Patterns

Look for:

  • try-catch blocks
  • null checks
  • 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_DEBUG for 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