Skip to main content

Effective Error Handling with Result Pattern

· 6 min read
Yusuf Çırak
Software Developer

Result Pattern

Herkese merhabalar,

Geliştirmiş olduğumuz uygulamalarda, en azından C# dünyasında çoğunlukla hata yönetimi için Exception objeleri kullanırız. Throw statement’ını kullanarak ve uygulamayı sarmallayan bir try-catch bloğu içerisinde kendi oluşturmuş olduğumuz hata tiplerine göre aksiyonlar alırız.

Bu yöntemi kullanmanın bazı avantajları ve dezavantajları vardır.

Exception avantajları

Kullanım Kolaylığı

Throw statement’ı ile Exception objesi oluşturulup fırlatılır. Bu da hata yönetimi kolaylaştırır.

Devre Kesici Görevi

Exception’ların amacı bu olduğu için, kod akışında istenilen yerde akış kesilebilir.

Exception objesinin “hafif” olması

Sadece hata mesajı parametresi ile oluşturulan, tekrar fırlatılmayan Exception’lar aslında yeterince hafiftir.

Exception’ların büyümesine sebep olan şey stack trace ve iç içe inner exception’ların oluşturduğu object tree’dir.

Exception dezavantajları

Kod okunurluğunun azalması

Kod okunurluğu azalabilir çünkü uygulamanın herhangi bir yerinde akış kesiliyor olabilir.

Bu da hiçbir methodun içeriğinde hata yönetimi ile ilgili bir kod olup olmadığını yanıtlamaz, ya da hangi tür hataların olduğunu da yanıtlamaz çünkü ilgili methodun imzasına baktığımızda sadece happy path’i görebiliriz.

Heap Allocation

Ne kadar çok Exception kullanılırsa allocation artacaktır, buna bağlı olarak da Garbage Collector’ın çalışma sıklığı artacaktır.

Bu da CPU kullanımının anlık aşırı yükselmesine sebep olacaktır.

Throw Statement Performansı

Throw statement’ı, return statement’ına göre çok daha yavaş çalışır. Kendi ortamımda, .NET 8 versiyonuyla yapmış olduğum test sonuçları aşağıdaki gibidir:


| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------------------------------------- |-------------:|-----------:|-----------:|-------:|----------:|
| ReturnString_WithoutThrowingException | 8.386 ns | 0.2066 ns | 0.1832 ns | 0.0408 | 128 B |
| ReturnString_WithThrowingException | 5,466.732 ns | 32.1059 ns | 30.0319 ns | 0.0687 | 224 B |

Test için kullanmış olduğum kodları incelemek için tıklayınız.

Multi-thread senaryolar ve Exception

Exception fırlatılan bir fonksiyon, multi-thread bir senaryoda çalışacaksa, o hatanın internal olarak try-catch bloğunda yakalanması ve gerekiyorsa tekrar fırlatılması gerekir.

Aksi takdirde fırlatılan Exception, main thread tarafından yakalanmayacaktır ve aynı zamanda Exception’ı fırlatan thread sonlanacaktır.

Result Pattern

Result Pattern, hata yönetimini basitleştiren bir yaklaşımdır. Bir metodun sonucunda hem başarı durumunu hem de varsa hata bilgisini döndürerek, geliştiricilere daha temiz ve anlaşılır bir kod yapısı sunar. Bu model, hata yönetimini daha öngörülebilir ve etkili hale getirir.

2 adet farklı implementasyondan bahsedeceğiz.

Result<TValue>

    public sealed record Result<TValue>();

Neden record?

  • Record veri tipi, class’tan farklı olarak immutable’dır ve value equaility ile çalışır.

  • Sealed olmasının sebebi ise başka bir record’a kalıtım vermeyeceğinden dolayıdır. Bu da virtual metodları çağırmada, casting gibi konularda performans sağlar. Bu konuyla ilgili detaylı anlatımı linkteki yazıdan inceleyebilirsiniz.

  public sealed record Result<TValue>
{
/// <summary>
/// Gets the value of the result.
/// </summary>
public TValue Value { get; }

/// <summary>
/// Gets the error associated with a failed result.
/// </summary>
public Error Error { get; } = ErrorsCache.None;

/// <summary>
/// Gets a value indicating whether the result is a success.
/// </summary>
public bool IsSuccess { get; } = false;

/// <summary>
/// Gets a value indicating whether the result is a failure.
/// </summary>
public bool IsFailure => !IsSuccess;

}

  • TValue için herhangi bir constraint bulunmamakta.

  • Error değeri initial olarak ErrorsCachesınıfından geliyor ve değerleri default.

Constructor’lar private, Success, Failuregibi metotlar var.


private Result(TValue value)
{
Value = value;
IsSuccess = true;
}
private Result(Error error)
{
Value = default!;
Error = error;
}

public static Result<TValue> Success(TValue value) => new(value);

public static Result<TValue> Failure(Error error) => new(error);

Success ve Failure adlı iki adet statik metod yardımıyla instance oluşturuluyor.


/// <summary>
/// Implicitly converts a value to a success result.
/// </summary>
/// <param name="value">The value to convert to a result.</param>
public static implicit operator Result<TValue>(TValue value) => Success(value);

/// <summary>
/// Implicitly converts an error to a failure result.
/// </summary>
/// <param name="error">The error to convert to a result.</param>
public static implicit operator Result<TValue>(Error error) => Failure(error);

Implicit operatorleri ise Result<TValue>.Success(value) gibi uzun kod yazımını engellemek için.

Bunlar dışında da son olarak 2 adet Match metodumuz var.

    /// <summary>
/// Matches the result to a success or failure function.
/// </summary>
/// <typeparam name="TResult">The type of the result returned by the match functions.</typeparam>
/// <param name="success">The function to execute if the result is a success.</param>
/// <param name="failure">The function to execute if the result is a failure.</param>
/// <returns>The result of the executed function.</returns>
public TResult Match<TResult>(Func<TValue, TResult> success, Func<Error, TResult> failure)
=> this.IsSuccess ? success(this.Value) : failure(this.Error);

/// <summary>
/// Executes one of the provided actions based on the success or failure state of the current result.
/// </summary>
/// <param name="success">The action to execute if the result is successful, with the value as a parameter. This parameter is optional and defaults to null.</param>
/// <param name="failure">The action to execute if the result is a failure, with the error as a parameter. This parameter is optional and defaults to null.</param>
# nullable enable
public void Match(Action<TValue>? success = null, Action<Error>? failure = null)
{
if (this.IsSuccess)
{
success?.Invoke(this.Value);
}
else
{
failure?.Invoke(this.Error);
}
}

Bu metodlar da Fluent Match API sağlamak için.

Örneğin:

    var result = MyOperation(false);

result.Match(
() => Console.WriteLine("Operation was successful."),
error => Console.WriteLine($"Operation failed with error: {error.Detail}")
);

var result2 = MyOperation(true);

var httpResult = result.Match(
value => Results.Ok(value),
error => Results.StatusCode(error.Status)
);

Diğer Result tipimiz ise herhangi bir TValue parametresi almıyor. Yani herhangi bir veri taşımıyor. Sadece bir operasyonun başarılı olduğu ve sadece hata durumunda hatanın detayını taşıması gerektiği durumlarda kullanılabilir.

    public sealed record Result
{
public Error Error { get; } = ErrorsCache.None;

public bool IsSuccess { get; } = false;

public bool IsFailure => !IsSuccess;
}

private Result(Error error)
{
Error = error;
}
internal Result(bool isSuccess)
{
IsSuccess = isSuccess;
}

/// <returns>A cached success <see cref="Result"/>.</returns>
public static Result Success() => ResultsCache.Success;

/// <returns>A cached failure <see cref="Result"/>.</returns>
public static Result Failure() => ResultsCache.Failure;
public static Result Failure(Error error) => new(error);

Burada ekstradan cachelenmiş Success ve Failure Result objeleri var.

Bunun dışında implicit operator ve Match API’ları aynı şekilde bu objede de var.

Örneğimize gelecek olursak,


using YC.Result;
using Microsoft.AspNetCore.Http;

Result<string> GetGreeting(bool isSuccessful)
{
if (isSuccessful)
{
return "Hello, World!"; // creating successful result implicitly
}
else
{
// creating failure result implicitly
return Error.Create("Greeting Error", "Failed to generate greeting.", 500);

}
}

var result = GetGreeting(false);

var httpResult = result.Match(
value => Results.Ok(value),
error => Results.StatusCode(error.Status)
);

Bu kod örneğinde, GetGreeting methodundan gelen response’a göre 200 veya 500 HttpResult dönen bir obje elde ediyoruz.

Mevcut implementasyonu bir NuGet paketi olarak hazırladım.

Okuduğunuz için teşekkür ederim.

Faydalandığım kaynaklar: