W poprzednich odcinkach serii WP importer zbudowaliśmy moduły: Google Places API, WordPress REST, Entity Framework i bota w Selenium. Wszystko śmiga — dopóki nie wywalisz tego na produkcję i nie puścisz 10 tysięcy wizytówek. Wtedy okazuje się, że po dziesięciu minutach proces się przewrócił, bo w jednym komentarzu pojawiło się emoji, którego driver przeglądarki nie zjadł. I musisz zaczynać od zera.
Tego właśnie w .NET unikasz mechanizmem try/catch. W tym wpisie pokażę ci nie tylko samą składnię, ale też hierarchię wyjątków, filtr when, logowanie przez ILogger i custom exceptions — czyli wszystko, czego potrzebujesz, żeby twoja aplikacja konsolowa nie padała przy pierwszej lepszej awarii po stronie trzeciej.
Po co try/catch
Zasada jest prosta: try/catch zakładasz wszędzie tam, gdzie nie jesteś pewny, czy kod zadziała. Nie chodzi o to, żeby obudowywać nim każdą linijkę — to jest antywzorzec, który tylko zaciemnia kod. Chodzi o to, żeby zabezpieczać granice twojej aplikacji: miejsca, w których rozmawiasz ze światem zewnętrznym.
Klasyczne przykłady z tego projektu:
- wywołania HTTP do Google Places API — serwer może zwrócić 500, rate limit, timeout;
- zapis do bazy danych przez EF Core — constraint violation, zerwane połączenie, deadlock;
- bot w Selenium publikujący na WordPressie — silnik przeglądarki krztusi się na emoji, timeout na wolnym Wi-Fi, zmieniony DOM;
- serializacja/deserializacja JSON-a — niespodziewany format odpowiedzi.
Reszta logiki aplikacyjnej — mapowanie, walidacje własnych danych, operacje na kolekcjach — to kod, który sam piszesz i sam kontrolujesz. Tam try/catch nie potrzebujesz, bo jeśli tam leci wyjątek, to masz bug i chcesz, żeby aplikacja krzyknęła głośno.
Nie obudowuj
try/catchlogiki biznesowej, którą sam napisałeś. Obudowuj granice — HTTP, I/O, DB, zewnętrzne procesy. Reszta to ukrywanie własnych bugów.
W moim przypadku to bot okazał się najbardziej niestabilnym elementem. Dodawanie komentarzy przez Selenium czasem rzucało wyjątek, gdy trafił się komentarz z emoji. Nie chciałem tracić dodawania całej wizytówki z powodu jednego z pięćdziesięciu komentarzy — więc opakowałem pętlę w try/catch i jedziemy dalej.
Hierarchia wyjątków
Zanim wrzucisz catch (Exception ex), warto wiedzieć, że wyjątki w .NET tworzą drzewo. Na szczycie jest System.Exception, a pod nim — dziesiątki klas specjalizowanych:
SystemExceptionNullReferenceException,ArgumentException,InvalidOperationExceptionIOException→FileNotFoundException,DirectoryNotFoundException
HttpRequestException— wszystko, co rzucaHttpClientDbUpdateException,DbUpdateConcurrencyException— Entity FrameworkTaskCanceledException,OperationCanceledException— cancellation tokens i timeoutyJsonException— System.Text.Json
Reguła numer jeden: łap od najbardziej szczegółowego do najbardziej ogólnego. CLR przechodzi bloki catch po kolei i bierze pierwszy pasujący, więc jeśli dasz catch (Exception) jako pierwszy, wszystkie następne są martwym kodem.
try
{
var places = await _places.FindNearby(lat, lng, radius, type);
foreach (var place in places)
{
await _publisher.PublishAsync(place);
}
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning(ex, "Rate limit hit — retrying after 10s");
await Task.Delay(TimeSpan.FromSeconds(10));
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database error — skipping batch");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error");
throw;
}
Zwróć uwagę na ostatni catch — łapię Exception, loguję i rzucam dalej przez throw; (nie throw ex; — to resetuje stack trace). To jest siatka bezpieczeństwa: logujemy, co się stało, ale pozwalamy aplikacji zareagować wyżej albo zakończyć się z pełnym kontekstem.
Exception filter — when
Konstrukcja catch (Type ex) when (warunek) to często pomijany gem z C# 6. Filtr when pozwala warunkowo wejść w blok catch — jeśli warunek jest fałszywy, wyjątek leci dalej, tak jakby tego catcha nie było.
Kluczowa różnica w porównaniu do zwykłego if wewnątrz bloku:
// Źle — łapiemy i rzucamy, stack trace się zabrudzi
catch (HttpRequestException ex)
{
if (ex.StatusCode != HttpStatusCode.TooManyRequests)
throw;
// handle rate limit
}
// Dobrze — filtr when, czysty stack trace, zero rzucania
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// handle rate limit
}
Filtry są wyjątkowo przydatne przy rozróżnianiu statusów HTTP, kodów błędów SQL albo treści wyjątku:
catch (SqlException ex) when (ex.Number == 2627) // unique constraint
{
_logger.LogWarning("Duplikat — pomijamy {PlaceId}", place.Id);
}
catch (SqlException ex) when (ex.Number == -2) // timeout
{
_logger.LogError(ex, "DB timeout — cofam batch");
throw;
}
Custom exceptions
Gdy twoja domena rośnie, warto wprowadzić własne typy wyjątków. Po co? Dwa powody:
- Semantyka —
PlaceNotFoundExceptionmówi więcej niżException("not found"). - Warstwy przepuszczające — wyższe piętra aplikacji mogą łapać tylko te wyjątki, które ich dotyczą, a nie wszystko leci jako generyczny
Exception.
public class PlaceNotFoundException : Exception
{
public PlaceNotFoundException(string placeId)
: base($"Place '{placeId}' not found in Google API") { }
}
Dobre praktyki przy pisaniu własnych wyjątków:
- Dziedzicz z
Exception(nie zApplicationException— to zombie z .NET Framework). - Daj trzy konstruktory: domyślny, z
message, zmessageiinnerException. - Trzymaj dodatkowe dane w typowanych property, nie w
Datadictionary. - Jeśli projekt jest duży — zrób hierarchię bazową (np.
WpImporterException), żeby mieć jedno miejsce do łapania „moich” błędów.
public abstract class WpImporterException : Exception
{
protected WpImporterException(string message) : base(message) { }
protected WpImporterException(string message, Exception inner)
: base(message, inner) { }
}
public class PlaceNotFoundException : WpImporterException
{
public string PlaceId { get; }
public PlaceNotFoundException(string placeId)
: base($"Place '{placeId}' not found in Google API")
{
PlaceId = placeId;
}
}
Wyżej w pipeline robisz catch (WpImporterException) i wiesz, że to coś, co sam zgłosiłeś świadomie — w odróżnieniu od losowego HttpRequestException z trzewi biblioteki.
Logowanie zamiast połykania
Najgorsze, co możesz zrobić z wyjątkiem, to złapać go i zignorować. Pusty blok catch to bomba z opóźnionym zapłonem — aplikacja idzie dalej, ale nie wiesz, że coś się wywaliło, dane są niespójne, a ty siedzisz nad logiem trzy dni później i nie widzisz nawet śladu po problemie.
catch (Exception) { }— to jedno z najgorszych rzeczy, jakie możesz zrobić swojemu przyszłemu ja. Stracisz dzień debugowania każdego takiego miejsca.
Minimum, które musisz zrobić w catchu:
- Zaloguj przez
ILogger—LogError(ex, "komunikat {Kontekst}", dane)— pierwszym argumentem jest wyjątek, żeby logger serializował cały stack trace. - Podejmij decyzję — recover (retry, fallback), skip (loguj i jedź dalej), albo rethrow (
throw;). - Nie zmieniaj wyjątku bezcelowo — jeśli chcesz zamienić go na domenowy, użyj
throw new PlaceNotFoundException(id, innerException: ex);— to zachowa oryginalny stack trace wInnerException.
Przykład z projektu — dodawanie komentarzy przez bota:
foreach (var comment in comments)
{
try
{
await _bot.AddComment(listingId, comment);
}
catch (WebDriverException ex) when (ContainsEmoji(comment.Text))
{
_logger.LogWarning(ex, "Komentarz z emoji pominięty ({ListingId})", listingId);
continue;
}
catch (Exception ex)
{
_logger.LogError(ex, "Nieoczekiwany błąd dodawania komentarza ({ListingId})", listingId);
throw;
}
}
Pętla przepuszcza „znany zły komentarz” i leci dalej, ale gdy wywali się coś nieoczekiwanego — rethrowuje, żeby nie ukrywać prawdziwych bugów. Tak wyglądają trzy linijki, które w moim projekcie uratowały import kilkuset wizytówek.
Wyjątek nieobsłużony nie jest błędem — błędem jest wyjątek połknięty w ciszy.
Co dalej?
To ostatni odcinek serii WP importer — w czternastu częściach przeszliśmy drogę od pustego Program.cs do działającego bota, który importuje dane z Google Places na WordPressa, zapisuje je do bazy i radzi sobie z emoji w komentarzach.
Kilka tematów, które warto dotknąć dalej, jeśli chcesz zrobić to produkcyjnie:
- Polly — retry, circuit breaker, timeout jako polityka zamiast ręcznego
Task.Delay; - MediatR + pipeline behavior — jeden centralny
TryCatchBehavior, który loguje i mapuje wyjątki na wyniki; - Global exception handler — w ASP.NET Core jest
UseExceptionHandler, w konsoli można sobie zrobić własny wrapper wokółmain; - Structured logging (Serilog) — zamiast sklejania stringów, logujesz właściwości i filtrujesz je w Seq / Elastic.
Jeśli seria ci pomogła, zostaw lajka i napisz w komentarzu, jaki projekt zrobić następny. Kod całego importera jest na moim GitHubie (jarmatys/WPImporter), możesz go zforkować i dalej rozbudowywać.



