Przykład
Przygotowałem krótki kod w C#. Jest to aplikacja konsolowa, która w statycznej metodzie Main wywołuje metodę odpowiedzialną za wysyłanie maili. Metoda Send powinna wykonywać logikę odpowiedzialną za wysłanie maila. W ciele metody Send jest wywoływana jeszcze metoda Connect na obiekcie klasy HostSmtp, która w naszym przypadku symuluje tylko wystąpienie błędu z wiadomością Cannot connect.
using System;
namespace App
{
public class HostSmtp
{
public void Connect()
{
throw new Exception("Cannot connect.");
}
}
public class EmailSender
{
public void Send()
{
new HostSmtp().Connect();
}
}
public class Program
{
static void Main(string[] args)
{
new EmailSender().Send();
}
}
}
Po uruchomieniu tego programu otrzymujemy następujący wynik w konsoli:
Unhandled Exception: System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 17
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 25
To akurat nie było zaskoczeniem, można było taką sytuację przewidzieć. Przyznasz jednak, że takie zachowanie aplikacji po pierwsze nie jest zbyt przyjazne dla użytkownika, a po drugie nie powiadamia administratora o jakimś nieprzewidzianym zachowaniu aplikacji. Dlatego, jeżeli jest jakiś kod, gdzie spodziewamy się otrzymać błąd, to możemy wywołać ten kod w bloku try catch.
Czy użycie bloku try catch wystarczy?
Dzięki zastosowaniu bloku try catch nasze wywołanie w metodzie Send może wyglądać w ten sposób:
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception)
{
}
}
}
Ok, teraz uruchamiamy ponownie naszą aplikację. Nie ma żadnych błędów, więc chyba wszystko dobrze? No, nie do końca :) Właściwie to takie, rozwiązanie jest jeszcze gorsze niż poprzednie, ponieważ co prawda przechwyciliśmy wyjątek, ale nie został on przez nas odpowiednio obsłużony. Mail, który miał być wysłany - nie został wysłany oraz nie zostaliśmy o tym fakcie poinformowani. Także, to rozwiązanie jest fatalne. Staraj się unikać takiej sytuacji, nigdy nie ma dobrego powodu, aby użyć takiego właśnie zapisu. W takim razie jak obsłużyć ten wyjątek? Mamy tutaj 2 problemy, które musimy rozwiązać. Chcemy najpierw, aby administrator systemu mógł przejrzeć szczegółowe informacje o błędzie oraz, żeby użytkownikowi wyświetlił się bardziej przyjazny komunikat, tutaj w szczególności bez informacji o całym stosie błędu.
Zapisywanie błędów
Pierwszy problem można łatwo rozwiązać, wystarczy użyć jednego z wielu frameworków do logowania danych i zapisać wszystkie szczegółowe informacje o błędzie, tak żeby łatwo dało się zdiagnozować przyczynę błędu. Ten kod może wyglądać tak:
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
}
}
}
Udało się zapisać do pliku wszystkie szczegółowe informacje o błędzie, jest już lepiej, ale dalej mamy problem z tym, że błąd został przechwycony, ale nie został prawidłowo obsłużony. Dalej użytkownik nie wie, że operacja się nie powiodła.
Popularne sposoby obsługi wyjątków
Jest dużo sposobów, jak można obsłużyć jeszcze ten błąd. Przyjrzyjmy się 4 najczęściej używanym rozwiązaniom, a następnie wybierzemy najlepsze.
1 sposób "throw ex":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw ex;
}
}
}
Wynik:
Unhandled Exception: System.Exception: Cannot connect.
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Uwagi:
Niestety używając throw ex; nie mamy pełnej informacji o błędzie, który wystąpił. Nie ma informacji o tym, że wyjątek został rzucony w metodzie Connect w linii 9, przez to zdiagnozowanie błędu może być trudniejsze. Ten sposób jest zły.
2 sposób "throw new Exception("Some exception.")":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw new Exception("Some exception.");
}
}
}
Wynik:
Unhandled Exception: System.Exception: Some exception.
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Uwagi:
Podobnie jak w poprzednim przykładzie, gdy używamy throw new Exception, tracimy informację o błędzie, który wystąpił wcześniej. Także, ten sposób również jest zły.
3 sposób "throw":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw;
}
}
}
Wynik:
Unhandled Exception: System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Uwagi:
I o to nam chodziło. W przypadku użycia samego throw mamy cały stack trace, wszystkie informacje o wcześniejszych błędach. Jak widzisz tylko w tym przypadku, nie przepadła informacja o pierwszym błędzie, który wystąpił w metodzie Connect w lini 9. Także, z tych 3 sposobów, często spotykanych w różnych aplikacjach, tylko ten sposób jest prawidłowy.
4 sposób "throw new Exception("Some exception.", ex)":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw new Exception("Some exception.", ex);
}
}
}
Wynik:
Unhandled Exception: System.Exception: Some exception. ---> System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 19
--- End of inner exception stack trace ---
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Uwagi:
Jak widzisz, jest jeszcze 4 sposób obsługi wyjątków. Ten sposób jest również dobry, ale raczej używa się go tylko wtedy gdy chcemy rzucić wyjątek innego typu.
Wyświetlenie komunikatu użytkownikowi
Oczywiście użytkownikowi nie możemy wyświetlić takiego błędu, powinniśmy wyświetlić odpowiedni komunikat, co najczęściej robi się w metodzie globalnej, która w zależności od rodzaju aplikacji wygląda inaczej. W naszym przypadku, w aplikacji konsolowej może wyglądać w ten sposób:
public class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
new EmailSender().Send();
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Logger.Error(ex. ExceptionObject);
Console.WriteLine("Wystąpił nieobsłużony błąd.");
Environment.Exit(1);
}
}
Lub możesz obsłużyć bezpośrednie wywołanie:
public class Program
{
static void Main(string[] args)
{
try
{
new EmailSender().Send();
}
catch (Exception ex)
{
Console.WriteLine("Nie udało się wysłać maila.");
}
}
}
Dzięki takiemu rozwiązaniu aplikacja działa dalej poprawnie, a użytkownik zostaje poinformowany o błędzie. Wyświetliłem tutaj ogólny błąd, ale dzięki właściwości Message każdego wyjątku możesz też wyświetlić bardziej szczegółowe informacje.
Także, cały kod może wyglądać w ten sposób:
using System;
namespace App
{
public class HostSmtp
{
public void Connect()
{
throw new Exception("Cannot connect.");
}
}
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Zapisanie wszystkich szczegółowych informacji o błędzie
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw;
}
}
}
public class Program
{
static void Main(string[] args)
{
try
{
new EmailSender().Send();
}
catch (Exception ex)
{
Console.WriteLine("Nie udało się wysłać maila.");
}
}
}
}
PODSUMOWANIE:
Mam nadzieję, że w tym artykule udało mi się pokazać Ci jak prawidłowo obsługiwać wyjątki w C#. Pamiętaj, żeby nigdy nie pozostawiać pustej klauzuli catch, ponieważ wtedy błąd zostanie stłumiony, a nie zostanie w żaden sposób obsłużony. Zawsze powinno się zapisywać jak najwięcej informacje o błędach, dzięki czemu ich wykrycie oraz poprawienie będzie dużo łatwiejsze. Zaoszczędzi Ci to w przyszłości sporo czasu. Pokazałem Ci kilka sposobów jak takie wyjątki w odpowiedni sposób obsłużyć. Pamiętaj, że zazwyczaj najlepszym sposobem jest użycie po prostu throw, dzięki czemu nie zostaną utracone żadne informacje o wcześniejszym błędzie.
Poprzedni artykuł - Jak Tworzyć Nowe Klasy w Visual Studio Domyślnie z Modyfikatorem Public?.
Następny artykuł - Proste Logowanie Danych Do Pliku w C# Za Pomocą Biblioteki NLog.