Skip to content

NOPE-PRO Best Practices

Follow these guidelines to write clean, maintainable, and performant code with NOPE-PRO.

General Principles

1. Make Errors Explicit

1// ❌ Bad - Hidden errors
2public User GetUser(string id)
3{
4    try { return LoadUser(id); }
5    catch { return null; }  // What went wrong?
6}
7
8// ✅ Good - Explicit errors
9public Result<User, string> GetUser(string id)
10{
11    return LoadUser(id)
12        .MapError(e => $"Failed to load user {id}: {e}");
13}

2. Use Meaningful Error Types

1// ❌ Bad - Generic strings
2Result<Data, string> LoadData() => "Error";
3
4// ✅ Good - Specific error types
5public enum DataError 
6{ 
7    NotFound, 
8    Corrupted, 
9    AccessDenied 
10}
11Result<Data, DataError> LoadData() => DataError.NotFound;
12
13// ✅ Better - Rich error objects
14public class DataError
15{
16    public ErrorCode Code { get; }
17    public string Message { get; }
18    public Dictionary<string, object> Context { get; }
19}

3. Chain Operations, Don't Nest

1// ❌ Bad - Nested if statements
2public Result<Order, string> ProcessOrder(Order order)
3{
4    if (ValidateOrder(order))
5    {
6        if (CheckInventory(order))
7        {
8            if (ChargePayment(order))
9            {
10                return ShipOrder(order);
11            }
12            return "Payment failed";
13        }
14        return "Out of stock";
15    }
16    return "Invalid order";
17}
18
19// ✅ Good - Chained operations
20public Result<Order, string> ProcessOrder(Order order)
21{
22    return ValidateOrder(order)
23        .Bind(CheckInventory)
24        .Bind(ChargePayment)
25        .Bind(ShipOrder);
26}

Result<T,E> Best Practices

1. Choose the Right Creation Method

1// Direct creation for known outcomes
2Result<int, string> Divide(int a, int b)
3{
4    return b == 0 
5        ? "Cannot divide by zero"  // Implicit failure
6        : a / b;                   // Implicit success
7}
8
9// Use Of() for exception-prone code
10Result<Config, string> LoadConfig()
11{
12    return Result.Of(
13        () => JsonUtility.FromJson<Config>(configJson),
14        ex => $"Config parse error: {ex.Message}"
15    );
16}
17
18// Use SuccessIf for boolean conditions
19Result<User, string> ValidateAge(User user)
20{
21    return Result.SuccessIf(
22        user.Age >= 18, 
23        user, 
24        "User must be 18 or older"
25    );
26}

2. Handle Both Success and Failure

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);
12
13// ✅ Also good - Chain with error handling
14LoadData()
15    .Map(Process)
16    .MapError(error => {
17        LogError(error);
18        return error;
19    });

3. Add Context to Errors

1// ❌ Bad - Generic errors
2public Result<Data, string> LoadUserData(string userId)
3{
4    return LoadFromDatabase(userId)
5        .Bind(ParseData)
6        .Bind(ValidateData);  // Which step failed?
7}
8
9// ✅ Good - Contextual errors
10public Result<Data, string> LoadUserData(string userId)
11{
12    return LoadFromDatabase(userId)
13        .MapError(e => $"Database error for user {userId}: {e}")
14        .Bind(ParseData)
15        .MapError(e => $"Parse error for user {userId}: {e}")
16        .Bind(ValidateData)
17        .MapError(e => $"Validation error for user {userId}: {e}");
18}

Maybe<T> Best Practices

1. Use Maybe Instead of Null

1// ❌ Bad - Nullable references
2public User FindUser(string id)
3{
4    return users.FirstOrDefault(u => u.Id == id); // Can be null
5}
6
7// ✅ Good - Explicit optionality
8public Maybe<User> FindUser(string id)
9{
10    return users.TryFirst(u => u.Id == id);
11}

2. Chain Maybe Operations

1// ❌ Bad - Multiple null checks
2public string GetManagerEmail(string employeeId)
3{
4    var employee = FindEmployee(employeeId);
5    if (employee == null) return "No email";
6    
7    var manager = employee.Manager;
8    if (manager == null) return "No email";
9    
10    var email = manager.Email;
11    if (string.IsNullOrEmpty(email)) return "No email";
12    
13    return email;
14}
15
16// ✅ Good - Chained Maybe
17public string GetManagerEmail(string employeeId)
18{
19    return FindEmployee(employeeId)
20        .Bind(e => e.Manager)
21        .Bind(m => m.Email)
22        .Where(e => !string.IsNullOrEmpty(e))
23        .Or("No email");
24}

3. Use Collection Helpers

1// ❌ Bad - Manual null checks
2var dict = GetSettings();
3if (dict.ContainsKey("ApiUrl"))
4{
5    var url = dict["ApiUrl"];
6    if (!string.IsNullOrEmpty(url))
7    {
8        UseUrl(url);
9    }
10}
11
12// ✅ Good - TryFind helper
13GetSettings()
14    .TryFind("ApiUrl")
15    .Where(url => !string.IsNullOrEmpty(url))
16    .Execute(UseUrl);

Async Best Practices

1. Use Async Methods When Available

1// ❌ Bad - Blocking async
2public Result<Data, string> LoadData()
3{
4    var task = LoadDataAsync();
5    return task.Result;  // Blocks thread!
6}
7
8// ✅ Good - Async all the way
9public async UniTask<Result<Data, string>> LoadDataAsync()
10{
11    return await FetchFromServerAsync()
12        .Bind(ProcessDataAsync)
13        .Map(TransformDataAsync);
14}

2. Mix Sync and Async Appropriately

1// ✅ Good - Only async where needed
2public async UniTask<Result<User, string>> GetUserAsync(string id)
3{
4    return ValidateId(id)              // Sync validation
5        .Bind(FetchUserAsync)     // Async network call
6        .Map(TransformUser)            // Sync transformation
7        .Tap(CacheUserAsync);     // Async caching
8}

3. Handle Cancellation

1public async UniTask<Result<Data, string>> LoadDataAsync(CancellationToken ct)
2{
3    return await Result.OfAsync(
4        async () => {
5            ct.ThrowIfCancellationRequested();
6            var data = await FetchAsync(ct);
7            ct.ThrowIfCancellationRequested();
8            return await ProcessAsync(data, ct);
9        },
10        ex => ex is OperationCanceledException 
11            ? "Operation cancelled" 
12            : $"Error: {ex.Message}"
13    );
14}

Performance Best Practices

1. Avoid Unnecessary Allocations

1// ❌ Bad - Creates intermediate Results
2public Result<string, Error> Process(string input)
3{
4    return Result<string, Error>.Success(input)
5        .Map(s => s.Trim())
6        .Map(s => s.ToUpper())
7        .Map(s => s.Replace(" ", "_"));
8}
9
10// ✅ Good - Single transformation
11public Result<string, Error> Process(string input)
12{
13    return Result<string, Error>.Success(
14        input.Trim().ToUpper().Replace(" ", "_")
15    );
16}

2. Use Structs for Error Types

1// ✅ Good - Struct for simple errors
2public readonly struct ValidationError
3{
4    public string Field { get; }
5    public string Message { get; }
6    
7    public ValidationError(string field, string message)
8    {
9        Field = field;
10        Message = message;
11    }
12}
13
14// Use class only when needed (inheritance, large data)
15public class ComplexError : IError
16{
17    public List<string> Details { get; }
18    public Exception InnerException { get; }
19}

3. Cache Expensive Operations

1public class DataService
2{
3    private readonly Dictionary<string, Result<Data, Error>> cache = new();
4    
5    public Result<Data, Error> GetData(string key)
6    {
7        return cache.TryGetValue(key, out var cached)
8            ? cached
9            : LoadData(key).Tap(result => cache[key] = result);
10    }
11}

Visual Debugging Best Practices

1. Use Meaningful Flow Names

1// ❌ Bad - Generic names
2result.EnableDebug("Flow1")
3result.EnableDebug("Process")
4
5// ✅ Good - Descriptive names
6result.EnableDebug($"LoadUser_{userId}")
7result.EnableDebug($"ProcessOrder_{orderId}_{timestamp}")
8result.EnableDebug($"PaymentFlow_{transactionId}")

2. Add Metadata for Context

1// ✅ Good - Rich debugging context
2public Result<Order, Error> ProcessOrder(OrderRequest request)
3{
4    return ValidateRequest(request)
5        .EnableDebug("ProcessOrder", new
6        {
7            OrderId = request.OrderId,
8            CustomerId = request.CustomerId,
9            Amount = request.TotalAmount,
10            ItemCount = request.Items.Count,
11            Timestamp = DateTime.Now
12        })
13        .Bind(ExecuteOrder);
14}

3. Debug Strategically

1// Debug at service boundaries
2public class OrderService
3{
4    public Result<Order, Error> CreateOrder(OrderRequest request)
5    {
6        return InternalCreateOrder(request)
7            .EnableDebug($"OrderService.CreateOrder");
8    }
9}
10
11// Debug complex operations
12public Result<Report, Error> GenerateComplexReport(ReportParams params)
13{
14    return LoadData(params)
15        .Bind(ValidateData)
16        .Bind(TransformData)
17        .Bind(AggregateData)
18        .Bind(FormatReport)
19        .EnableDebug($"ComplexReport_{params.ReportId}");
20}
21
22// Conditional debugging in production
23#if DEBUG || DEVELOPMENT_BUILD
24    .EnableDebug("DetailedFlow")
25#else
26    .EnableDebug("ProductionFlow", minimalMetadata)
27#endif

Testing Best Practices

1. Test Success and Failure Paths

1[Test]
2public void ProcessOrder_ValidOrder_ReturnsSuccess()
3{
4    var order = CreateValidOrder();
5    var result = ProcessOrder(order);
6    
7    Assert.IsTrue(result.IsSuccess);
8    Assert.AreEqual(order.Id, result.Value.Id);
9}
10
11[Test]
12public void ProcessOrder_InvalidOrder_ReturnsExpectedError()
13{
14    var order = CreateInvalidOrder();
15    var result = ProcessOrder(order);
16    
17    Assert.IsTrue(result.IsFailure);
18    Assert.AreEqual(OrderError.ValidationFailed, result.Error);
19}

2. Test Error Recovery

1[Test]
2public void LoadConfig_FileNotFound_UsesDefault()
3{
4    var result = LoadConfigWithFallback("nonexistent.json");
5    
6    Assert.IsTrue(result.IsSuccess);
7    Assert.AreEqual("Default", result.Value.Name);
8}

3. Mock Result-Returning Methods

1public interface IUserService
2{
3    Result<User, Error> GetUser(string id);
4}
5
6[Test]
7public void TestWithMockedResult()
8{
9    var mockService = new Mock<IUserService>();
10    mockService
11        .Setup(s => s.GetUser("123"))
12        .Returns(Result<User, Error>.Success(new User { Id = "123" }));
13    
14    // Test your code with mocked service
15}

Common Patterns

1. Repository Pattern

1public interface IRepository<T, TId>
2{
3    Result<T, RepositoryError> GetById(TId id);
4    Result<Unit, RepositoryError> Save(T entity);
5    Maybe<T> Find(Expression<Func<T, bool>> predicate);
6}

2. Validation Pipeline

1public static class Validation
2{
3    public static Result<T, ValidationError> Validate<T>(
4        T value,
5        params IValidator<T>[] validators)
6    {
7        return validators.Aggregate(
8            Result<T, ValidationError>.Success(value),
9            (result, validator) => result.Bind(validator.Validate)
10        );
11    }
12}

3. Error Aggregation

1public class ErrorAggregator<T>
2{
3    private readonly List<T> errors = new();
4    
5    public Result<Unit, List<T>> ToResult()
6    {
7        return errors.Any() 
8            ? Result<Unit, List<T>>.Failure(errors)
9            : Result<Unit, List<T>>.Success(Unit.Value);
10    }
11}

Anti-Patterns to Avoid

1. Don't Throw Inside Result Methods

1// ❌ Bad - Defeats the purpose
2public Result<Data, Error> Process(Input input)
3{
4    if (input == null)
5        throw new ArgumentNullException();  // Don't throw!
6    
7    return ProcessInternal(input);
8}
9
10// ✅ Good - Return failure
11public Result<Data, Error> Process(Input input)
12{
13    if (input == null)
14        return new Error("Input cannot be null");
15    
16    return ProcessInternal(input);
17}

2. Don't Access Value Without Checking

1// ❌ Bad - Can throw
2var result = LoadData();
3var processed = Process(result.Value);  // Throws if failure!
4
5// ✅ Good - Check first or use Match
6var result = LoadData();
7result.Match(
8    onSuccess: data => { Process(data); return Unit.Value; },
9    onFailure: error => { HandleError(error); return Unit.Value; }
10);

3. Don't Mix Result and Exceptions

1// ❌ Bad - Inconsistent error handling
2public Result<Data, Error> LoadData()
3{
4    try
5    {
6        var data = GetData();
7        if (data == null)
8            return new Error("No data");  // Result error
9        
10        return Process(data);  // Might throw!
11    }
12    catch (Exception ex)
13    {
14        throw;  // Now throwing exceptions!
15    }
16}
17
18// ✅ Good - Consistent Result usage
19public Result<Data, Error> LoadData()
20{
21    return Result.Of(GetData, ex => new Error(ex))
22        .Ensure(data => data != null, new Error("No data"))
23        .Bind(data => Result.Of(
24            () => Process(data),
25            ex => new Error(ex)
26        ));
27}

Summary

Following these best practices will help you:

  • Write more maintainable code
  • Handle errors consistently
  • Improve performance
  • Make debugging easier
  • Create better tests

Remember: Make the success path and failure path equally important and explicit.