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ę! ✨