Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

Obsługa błędów to kluczowy element każdej aplikacji! W 2015 roku miałeś try/catch/finally i podstawowe exceptions. W 2026 roku masz exception filters (when), Result pattern jako alternatywę dla exceptions, i nowoczesne podejście do error handling!

W tym wpisie poznasz try/catch/finally podstawy, exception filters z when, custom exceptions, Result pattern jako alternatywę, i global exception handling!

📅 Timeline - ewolucja error handling
  • C# 1.0 (2002) - Try/catch/finally, throw
  • C# 6.0 (2015) - 🔥 Exception filters (when clause)
  • C# 7.0 (2017) - Throw expressions
  • Modern era (2020+) - Result pattern, Railway-oriented programming
  • ASP.NET Core (2016+) - Middleware-based global exception handling
Try/Catch/Finally - podstawy

Podstawowa składnia

// Try/catch - obsługa wyjątków
try
{
    // Kod który może rzucić exception
    int result = int.Parse("abc");  // Rzuci FormatException
}
catch (FormatException ex)
{
    // Obsługa konkretnego typu exception
    Console.WriteLine($"Błąd formatu: {ex.Message}");
}
catch (Exception ex)
{
    // Obsługa wszystkich innych exceptions
    Console.WriteLine($"Nieoczekiwany błąd: {ex.Message}");
}
finally
{
    // Wykonuje się ZAWSZE (nawet gdy jest exception)
    Console.WriteLine("Cleanup code");
}

// Kolejność catch: od najbardziej specyficznego do najbardziej ogólnego!

Multiple catch blocks

// Różne typy exceptions - różna obsługa
public void ProcessFile(string path)
{
    try
    {
        var content = File.ReadAllText(path);
        var number = int.Parse(content);
        var result = 100 / number;
    }
    catch (FileNotFoundException ex)
    {
        Console.WriteLine($"Plik nie istnieje: {ex.FileName}");
    }
    catch (FormatException ex)
    {
        Console.WriteLine($"Nieprawidłowy format liczby: {ex.Message}");
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine("Nie można dzielić przez zero!");
    }
    catch (IOException ex)
    {
        Console.WriteLine($"Błąd I/O: {ex.Message}");
    }
    catch (Exception ex)
    {
        // Catch-all - wszystkie inne błędy
        Console.WriteLine($"Nieoczekiwany błąd: {ex.Message}");
        throw;  // Re-throw exception
    }
}

Finally - cleanup code

// Finally - zawsze się wykonuje
FileStream file = null;
try
{
    file = File.OpenRead("data.txt");
    // Przetwarza plik...
}
catch (IOException ex)
{
    Console.WriteLine($"Błąd: {ex.Message}");
}
finally
{
    // ZAWSZE zamknij plik, nawet gdy był exception
    file?.Dispose();
}

// Nowoczesna alternatywa: using statement
using (var file = File.OpenRead("data.txt"))
{
    // Przetwarza plik...
}  // Automatycznie Dispose() w finally

// C# 8+ - using declaration (jeszcze lepsze!)
using var file2 = File.OpenRead("data.txt");
// Dispose() na końcu scope

Throw vs throw ex

// throw - zachowuje stack trace (ZALECANE)
try
{
    DoSomething();
}
catch (Exception ex)
{
    LogError(ex);
    throw;  // ✅ Re-throw z zachowanym stack trace
}

// throw ex - NISZCZY stack trace (NIE UŻYWAJ!)
try
{
    DoSomething();
}
catch (Exception ex)
{
    LogError(ex);
    throw ex;  // ❌ BŁĄD - stack trace zaczyna się tutaj, tracisz oryginalny!
}

// Wrap exception - nowy exception z InnerException
try
{
    DoSomething();
}
catch (Exception ex)
{
    throw new ApplicationException("Błąd w DoSomething", ex);  // ✅ ex jako InnerException
}
🔥 Exception Filters - when clause (C# 6)
🎉 C# 6 - Exception Filters

catch (Exception ex) when (condition) - catch TYLKO gdy warunek spełniony!

// Exception filters - catch z warunkiem
try
{
    await httpClient.GetAsync(url);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    Console.WriteLine("Zasób nie znaleziony (404)");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
    Console.WriteLine("Brak autoryzacji (401)");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError)
{
    Console.WriteLine("Błąd serwera (500)");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Inny błąd HTTP: {ex.StatusCode}");
}

// Bez when - trzeba by było:
try
{
    await httpClient.GetAsync(url);
}
catch (HttpRequestException ex)
{
    if (ex.StatusCode == HttpStatusCode.NotFound)
        Console.WriteLine("404");
    else if (ex.StatusCode == HttpStatusCode.Unauthorized)
        Console.WriteLine("401");
    // ... brzydkie zagnieżdżone if-y
}

When filters - praktyczne przykłady

// Przykład 1: Retry logic dla transient errors
int retryCount = 0;
while (true)
{
    try
    {
        await SaveToDatabase(data);
        break;  // Success
    }
    catch (SqlException ex) when (IsTransientError(ex) && retryCount < 3)
    {
        retryCount++;
        Console.WriteLine($"Transient error, retry {retryCount}/3");
        await Task.Delay(1000 * retryCount);  // Exponential backoff
    }
}

bool IsTransientError(SqlException ex)
{
    // Kody błędów SQL które są przejściowe (network, timeout, etc.)
    int[] transientErrorCodes = { -1, -2, 1205, 4060, 40197, 40501, 40613 };
    return transientErrorCodes.Contains(ex.Number);
}

// Przykład 2: Logging z filtering
try
{
    ProcessOrder(order);
}
catch (Exception ex) when (LogException(ex))
{
    // Ten block NIGDY się nie wykona!
    // LogException zwraca false, więc filter nie przechodzi
}

bool LogException(Exception ex)
{
    _logger.LogError(ex, "Błąd podczas przetwarzania");
    return false;  // NIE catch - pozwól exception propagować dalej
}

// Zaleta: exception zostaje zalogowany BEZ modyfikacji stack trace!

// Przykład 3: Environment-based filtering
try
{
    RiskyOperation();
}
catch (Exception ex) when (!Debugger.IsAttached)
{
    // Catch TYLKO w production (nie w debuggerze)
    LogAndIgnore(ex);
}
// W debuggerze - exception propaguje do debuggera (możesz zobaczyć co poszło źle)

// Przykład 4: Property-based filtering
try
{
    ValidateUser(user);
}
catch (ValidationException ex) when (ex.Severity == Severity.Critical)
{
    // Obsłuż tylko critical validation errors
    NotifyAdmin(ex);
    throw;
}
catch (ValidationException ex) when (ex.Severity == Severity.Warning)
{
    // Warning - loguj i kontynuuj
    _logger.LogWarning(ex.Message);
}

When filters - zalety

// Zalety exception filters:

// 1. Stack trace preservation
catch (Exception ex) when (SomeCondition(ex))
{
    // Stack trace zachowany - filter nie modyfikuje!
}

// 2. Cleaner code
// Bez when:
catch (HttpException ex)
{
    if (ex.StatusCode == 404)
    {
        Handle404();
    }
    else
    {
        throw;  // Re-throw jeśli nie 404
    }
}

// Z when:
catch (HttpException ex) when (ex.StatusCode == 404)
{
    Handle404();  // Czytelniejsze!
}

// 3. Side effects w filter
catch (Exception ex) when (Log(ex))  // Log zwraca false
{
    // Nigdy się nie wykona, ale exception zostało zalogowane!
}
Custom Exceptions - własne typy wyjątków

Definiowanie custom exception

// Custom exception - dziedziczy po Exception
public class UserNotFoundException : Exception
{
    public int UserId { get; }
    
    public UserNotFoundException(int userId)
        : base($"User with ID {userId} not found")
    {
        UserId = userId;
    }
    
    public UserNotFoundException(int userId, Exception innerException)
        : base($"User with ID {userId} not found", innerException)
    {
        UserId = userId;
    }
}

// Użycie
public User GetUser(int id)
{
    var user = _repository.FindById(id);
    
    if (user == null)
        throw new UserNotFoundException(id);
    
    return user;
}

// Catch custom exception
try
{
    var user = GetUser(123);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine($"Nie znaleziono użytkownika ID: {ex.UserId}");
}

Exception hierarchy - hierarchia wyjątków

// Base exception dla całej domeny
public abstract class DomainException : Exception
{
    protected DomainException(string message) : base(message) { }
    protected DomainException(string message, Exception inner) : base(message, inner) { }
}

// Specific exceptions
public class ValidationException : DomainException
{
    public Dictionary<string, string> Errors { get; }
    
    public ValidationException(Dictionary<string, string> errors)
        : base("Validation failed")
    {
        Errors = errors;
    }
}

public class BusinessRuleException : DomainException
{
    public string RuleName { get; }
    
    public BusinessRuleException(string ruleName, string message)
        : base(message)
    {
        RuleName = ruleName;
    }
}

public class InsufficientFundsException : BusinessRuleException
{
    public decimal Required { get; }
    public decimal Available { get; }
    
    public InsufficientFundsException(decimal required, decimal available)
        : base("InsufficientFunds", $"Required: {required}, Available: {available}")
    {
        Required = required;
        Available = available;
    }
}

// Użycie - catch na różnych poziomach
try
{
    ProcessPayment(order);
}
catch (InsufficientFundsException ex)
{
    // Najbardziej specyficzny
    Console.WriteLine($"Brak środków: {ex.Required - ex.Available} PLN");
}
catch (BusinessRuleException ex)
{
    // Ogólniejszy
    Console.WriteLine($"Naruszono regułę biznesową: {ex.RuleName}");
}
catch (DomainException ex)
{
    // Najogólniejszy
    Console.WriteLine($"Błąd domenowy: {ex.Message}");
}

Best practices - custom exceptions

// ✅ DO - Zalecane:

// 1. Nazwa kończy się na "Exception"
public class OrderNotFoundException : Exception { }  // ✅

// 2. Dziedziczy po Exception (lub innym exception)
public class MyException : Exception { }  // ✅

// 3. Ma konstruktory: (), (string), (string, Exception)
public class MyException : Exception
{
    public MyException() { }
    public MyException(string message) : base(message) { }
    public MyException(string message, Exception inner) : base(message, inner) { }
}

// 4. Dodaj właściwości dla dodatkowych danych
public class ValidationException : Exception
{
    public Dictionary<string, string> Errors { get; }  // ✅
}

// ❌ DON'T - Unikaj:

// 1. NIE używaj exceptions dla control flow
if (user == null)
    throw new UserNotFoundException();  // ❌ Użyj return null lub Result<T>

// 2. NIE catch Exception bez re-throw (chyba że global handler)
catch (Exception) { }  // ❌ Połyka wszystkie błędy!

// 3. NIE rzucaj System.Exception bezpośrednio
throw new Exception("Error");  // ❌ Użyj konkretnego typu

// 4. NIE używaj exceptions dla expected scenarios
public User GetUser(int id)
{
    var user = _repository.Find(id);
    if (user == null)
        throw new UserNotFoundException();  // ❌ To jest expected case!
    return user;
}

// Lepiej:
public User? GetUser(int id)
{
    return _repository.Find(id);  // null = not found ✅
}
Result Pattern - alternatywa dla exceptions

Problem z exceptions

// Problem: Exceptions dla expected errors
public User GetUser(int id)
{
    var user = _repository.FindById(id);
    
    if (user == null)
        throw new UserNotFoundException(id);  // Exception dla expected case!
    
    return user;
}

// Wywołanie - musisz catch
try
{
    var user = GetUser(123);
    ProcessUser(user);
}
catch (UserNotFoundException)
{
    Console.WriteLine("User not found");  // Expected scenario - brzydkie!
}

// Problemy:
// - Exceptions są DROGIE (performance)
// - Nie widać w sygnaturze że może "fail"
// - Control flow przez exceptions - anti-pattern
// - Expected errors !== exceptional situations

Result<T> pattern

// Result pattern - sukces lub błąd
public class Result<T>
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public string Error { get; }
    
    private Result(bool isSuccess, T value, string error)
    {
        IsSuccess = isSuccess;
        Value = value;
        Error = error;
    }
    
    public static Result<T> Success(T value)
        => new Result<T>(true, value, null);
    
    public static Result<T> Failure(string error)
        => new Result<T>(false, default, error);
}

// Użycie zamiast exceptions
public Result<User> GetUser(int id)
{
    var user = _repository.FindById(id);
    
    if (user == null)
        return Result<User>.Failure($"User {id} not found");  // ✅ Return error
    
    return Result<User>.Success(user);
}

// Wywołanie - jawna obsługa błędów
var result = GetUser(123);

if (result.IsSuccess)
{
    ProcessUser(result.Value);
}
else
{
    Console.WriteLine(result.Error);
}

// Zalety:
// ✅ Widoczne w sygnaturze że może fail
// ✅ Wymusza obsługę błędów
// ✅ Zero overhead (no exceptions)
// ✅ Expected errors ≠ exceptions

Result pattern - rozszerzenia

// Result z wieloma błędami
public class Result<T>
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public List<string> Errors { get; }
    
    public static Result<T> Success(T value)
        => new Result<T> { IsSuccess = true, Value = value, Errors = new() };
    
    public static Result<T> Failure(params string[] errors)
        => new Result<T> { IsSuccess = false, Errors = errors.ToList() };
}

// Result z custom error type
public class Result<T, TError>
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public TError Error { get; }
    
    public static Result<T, TError> Success(T value)
        => new() { IsSuccess = true, Value = value };
    
    public static Result<T, TError> Failure(TError error)
        => new() { IsSuccess = false, Error = error };
}

// Error type
public record ValidationError(string Field, string Message);

// Użycie
public Result<User, ValidationError> CreateUser(CreateUserRequest request)
{
    if (string.IsNullOrEmpty(request.Name))
        return Result<User, ValidationError>.Failure(
            new ValidationError("Name", "Name is required"));
    
    var user = new User { Name = request.Name };
    return Result<User, ValidationError>.Success(user);
}

Railway-oriented programming

// Railway-oriented programming - łańcuchowanie Result
public static class ResultExtensions
{
    public static Result<TOut> Map<TIn, TOut>(
        this Result<TIn> result,
        Func<TIn, TOut> mapper)
    {
        return result.IsSuccess
            ? Result<TOut>.Success(mapper(result.Value))
            : Result<TOut>.Failure(result.Error);
    }
    
    public static Result<TOut> Bind<TIn, TOut>(
        this Result<TIn> result,
        Func<TIn, Result<TOut>> binder)
    {
        return result.IsSuccess
            ? binder(result.Value)
            : Result<TOut>.Failure(result.Error);
    }
}

// Łańcuchowanie operacji
var result = GetUser(123)
    .Bind(user => ValidateUser(user))
    .Bind(user => UpdateUser(user))
    .Map(user => new UserDto(user.Name, user.Email));

if (result.IsSuccess)
{
    return result.Value;
}
else
{
    return BadRequest(result.Error);
}

// Jeśli którykolwiek step fail - cała pipeline zatrzymuje się! ✨
Feature Exceptions Result Pattern
Performance Wolniejsze (stack trace) Szybsze (zwykły return)
Widoczność błędów Nie widać w sygnaturze Widoczne w Result<T>
Wymuszanie obsługi Można zapomnieć catch Trzeba sprawdzić IsSuccess
Kiedy używać Exceptional situations Expected errors
Global Exception Handling

ASP.NET Core - middleware

// Global exception handler - middleware
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    
    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);  // Wywołaj następny middleware
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");
            await HandleExceptionAsync(context, ex);
        }
    }
    
    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var response = exception switch
        {
            ValidationException validationEx => new
            {
                StatusCode = 400,
                Message = "Validation failed",
                Errors = validationEx.Errors
            },
            UserNotFoundException notFoundEx => new
            {
                StatusCode = 404,
                Message = notFoundEx.Message
            },
            UnauthorizedAccessException => new
            {
                StatusCode = 401,
                Message = "Unauthorized"
            },
            _ => new
            {
                StatusCode = 500,
                Message = "Internal server error"
            }
        };
        
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = response.StatusCode;
        
        return context.Response.WriteAsJsonAsync(response);
    }
}

// Rejestracja w Program.cs
app.UseMiddleware<GlobalExceptionMiddleware>();

UseExceptionHandler - built-in ASP.NET

// Built-in exception handler
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>();
        var exception = exceptionHandler?.Error;
        
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
        logger.LogError(exception, "Unhandled exception");
        
        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "An error occurred",
            Detail = exception?.Message
        };
        
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

// W production - ukryj details
if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/error");
}
else
{
    app.UseDeveloperExceptionPage();  // Szczegóły w development
}

Problem Details - RFC 7807

// Problem Details - standard format dla API errors
public class ProblemDetailsExceptionHandler
{
    public static ProblemDetails CreateProblemDetails(Exception exception, HttpContext context)
    {
        return exception switch
        {
            ValidationException validation => new ValidationProblemDetails(validation.Errors)
            {
                Status = 400,
                Title = "Validation failed",
                Instance = context.Request.Path
            },
            
            UserNotFoundException notFound => new ProblemDetails
            {
                Status = 404,
                Title = "Resource not found",
                Detail = notFound.Message,
                Instance = context.Request.Path
            },
            
            UnauthorizedAccessException => new ProblemDetails
            {
                Status = 401,
                Title = "Unauthorized",
                Instance = context.Request.Path
            },
            
            _ => new ProblemDetails
            {
                Status = 500,
                Title = "Internal server error",
                Detail = context.RequestServices
                    .GetRequiredService<IHostEnvironment>()
                    .IsProduction() ? null : exception.Message,
                Instance = context.Request.Path
            }
        };
    }
}

// Response format (JSON):
// {
//   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
//   "title": "Validation failed",
//   "status": 400,
//   "errors": {
//     "Name": ["Name is required"]
//   },
//   "instance": "/api/users"
// }

Global error handling - best practices

// ✅ DO:

// 1. Loguj wszystkie unhandled exceptions
_logger.LogError(exception, "Unhandled exception");

// 2. Zwracaj różne kody HTTP dla różnych błędów
// 400 - ValidationException
// 401 - UnauthorizedAccessException
// 404 - NotFoundException
// 500 - wszystkie inne

// 3. Ukrywaj szczegóły w production
var message = _environment.IsProduction()
    ? "Internal server error"
    : exception.Message;

// 4. Używaj Problem Details (RFC 7807)
return new ProblemDetails { ... };

// 5. Dodaj correlation ID dla tracingu
var correlationId = Guid.NewGuid();
_logger.LogError(exception, "Error {CorrelationId}", correlationId);

// ❌ DON'T:

// 1. NIE zwracaj stack trace w production
// Detail = exception.ToString()  // ❌ Leak implementation details

// 2. NIE catch Exception bez logowania
catch (Exception) { }  // ❌ Silent failure

// 3. NIE używaj różnych formatów błędów
// Jeden endpoint zwraca JSON, drugi plain text - ❌
Podsumowanie

  • Try/catch/finally - podstawy, multiple catches, throw vs throw ex
  • 🔥 Exception filters (C# 6) - when clause, property-based filtering
  • Custom exceptions - hierarchia, best practices
  • Result pattern - alternatywa dla exceptions, railway-oriented
  • Global exception handling - middleware, Problem Details
  • Exceptions vs Result - kiedy czego używać

W kolejnym wpisie skupimy sie na pracy z plikami, tj. operacje odczytu/zapisu, operacje asynchroniczne, StreamReader/StreamWriter.