Ce sunt excepțiile?
Excepțiile sunt un mecanism care ne permite gestionarea erorilor neașteptate, sau excepționale, după cum le spune și numele. Excepțiile pot apărea doar la runtime, adică în timpul execuției codului, nu în momentul în care se compilează.
Deci ele nu reprezintă un mod prin care compilatorul să-ți spună că e ceva greșit în sintaxa codului tău în momentul în care îl scrii, ci vor aparea doar la momentul când codul este executat.
Apariția unei excepții depinde de starea la care se află codul la momentul respectiv. Prin “stare”, mă refer la valorile unor anumiți parametri ai metodei executate, valorile câmpurilor obiectului a cărui metodă se execută șamd.
Pe scurt, care sunt datele cu care codul nostru se execută.
De-asta astfel de excepții nu pot fi prinse la momentul compilării, pentru că la acel moment codul nostru nu se află într-o stare anume, ci doar exprimă o serie de instrucțiuni care trebuie executate.
În momentul în care vin și ceva parametri, sau date peste aceste instrucțiuni, există riscul apariției acestor excepții.
Hai să vedem un exemplu scris cu C#, dar mecanismul este general valabil pentru multe din limbajele ce adresă la paradigma de orientare pe obiect.
Încercăm să citim conținutul unui fișier de pe disc, folosind clasa File
din namespace-ul System.IO
.
var text = File.ReadAllText(@"C:\file.txt");
Dar înainte de a trece la citirea fișierului, nu verificăm existența lui la adresa indicată pe disc.
Execuția codului va duce astfel la “aruncarea” unei excepții.
Termenul de aruncare vine din traducerea cuvântului cheie throw.
Tratarea excepțiilor
Ei bine, pentru tratarea, sau prinderea excepțiilor, vom folosi alte 3 cuvinte cheie: try
, catch
și finally
. Hai să vedem.
try {
var text = File.ReadAllText(@"C:\file.txt");
}
catch (Exception)
{
Console.WriteLine("Citirea fisierului nu a fost posibila");
}
Ceea ce vedem în această bucățică de cod, este că am “îmbrăcat” codul potențial generator al unei excepții într-un bloc try-catch
, sau mai exact “încearcă/prinde”.
După cum probabil intuiești, ceea ce se află în interiorul lui try
este codul care noi credem că va putea arunca o excepție, de aici și denumirea de “încearcă”, iar catch
este ce facem mai departe când acea excepție chiar se întâmplă, de aici și denumirea de “prinde”. De obicei acest pas constă în afișarea unei erori, atunci când execuția codului nu mai poate continua în condițiile date.
În cazul în care pe parcursul execuției lui try
nu se aruncă nicio excepție, adică în cazul ăsta, să spunem că adresa fișierului e una corectă, atunci blocul catch
nu va mai fi executat. Execuția lui nu se face decât atunci când una din liniile din try
aruncă o excepție.
Spuneam mai devreme, că sunt 3 cuvinte cheie folosite pentru gestionarea excepțiilor, de 2 dintre ele am vorbit deja, al 3-lea fiind finally
sau “în final”.
Dacă în cazul blocului catch
codul nu e executat decât atunci când apare o excepție, în cazul lui finally
el este executat indiferent dacă o excepție apare sau nu.
try {
var text = File.ReadAllText(@"C:\file.txt");
}
catch (Exception)
{
Console.WriteLine("Citirea fișierului nu a fost posibilă");
}
finally
{
Console.WriteLine("Codul de aici este executat de fiecare dată");
}
În interiorul lui finally
de cele mai multe ori se vor curăța eventualele resurse existente care s-au folosit în try
.
Ce înseamnă asta?
În momentul în care folosim surse externe de date, cum ar fi fișierele de pe disc, sau o bază de date, obiectele folosite pentru interacțiunea cu aceste elemente externe, vor avea nevoie să fie “șterse”, pentru că altfel vor ocupa memoria inutil.
Ștergerea acestor obiecte se face prin execuția unei metode pe care o au toate obiectele în .NET și care poartă denumirea de Dispose()
. Astfel se face eliminarea lor și eliberarea memoriei.
Dacă ești curios să afli mai multe, am scris despre procesul de curățare al memoriei aici.
Aruncarea excepțiilor
Excepțiile pot fi aruncate din mai multe locuri, sau de mai mulți actori care iau parte la execuția codului aplicației tale.
Ele pot fi aruncate în primul rând din codul tău. Poți folosi cuvântul cheie throw
și instanța unei clase de tip Exception
pentru a arunca o excepție în aplicația ta.
int age = 16;
if(age < 18)
{
throw new Exception("Invalid age");
}
Ce se întâmplă în momentul în care o excepție este aruncată?
Ei bine în momentul în care execuția unei metode declanșează o excepție, se va căuta în lanțul de metode executate până la punctul respectiv, un bloc de tipul try-catch
.
Acest lanț de metode executate poartă denumirea de stacktrace, sau tradus în mod direct “urme de stivă”, dar nu e cea mai corectă sau descriptivă traducere, însă revenim imediat la asta.
Dacă pe lanțul de execuții nu se găsește nici un try-catch
, execuția se va opri și stacktrace-ul va fi întors ca și mesaj de eroare.
Dacă în schimb, undeva la nivelurile superioare de pe stacktrace, se găsește un try-catch
, atunci blocul catch
se execută, apoi se execută și blocul finally
dacă există, apoi se continuă execuția pe cursul normal.
Nu există o traducere în limba română care să descrie exact la ce se referă cuvântul “stracktrace”, dar într-o expresie sună cam așa: Urmele lăsate de cod pe parcursul execuției.
Acest stacktrace reprezintă mesajul de eroare al aplicației dacă nu există niciun punct în care excepția e tratată până la punctul de intrare în cod.

Exemple de excepții în .NET
Iată câteva exemple de excepții predefinite care există în .NET:
IOException
ArgumentNullException
DivideByZeroException
IndexOutOfRangeException
Exception
Ții minte mai devreme când îți povesteam că e posibil ca în cazul în care vei încerca să citești un fișier de pe disc folosind clasa File
, e posibil ca o excepție să fie aruncată în cazul în care fișerul sau directorul respectiv nu există? Ei bine, excepția aia va fi un IOException
, multe din erorile rezultate din operațiuni cu fișiere de pe disc vor fi de acest tip.
ArgumentNullException
poate fi aruncat atunci când unul din argumentele pasate unei metode este null
argumentul respectiv fiind obligatoriu. În sensul ăsta, și tu poți folosi ArgumentNullException
pentru a valida argumentele folosite pentru parametri unei metode astfel:
void Print(string text)
{
if(string.IsNullOrWhiteSpace(text))
{
throw new ArgumentNullException("text este obligatoriu");
}
//..
}
DivideByZeroException
e o excepție care va fi aruncată în cazul în care vei încerca să împarți un număr la 0, pentru că matematic împărțirea sau diviziunea cu 0 este imposibilă.
Sau IndexOutOfRangeException
care va fi aruncată dacă încerci să accesezi un element al unei colecții cu un index care nu există.
int[] numbers = { 1, 5, 3 }; //indecși: 0, 1, 2
Console.WriteLine(numbers[3]); //IndexOutOfRangeException
Am lăsat un pic la urmă Exception
pentru că este clasa de bază pentru toate celelalte tipuri în .NET. Toate celelalte tipuri de excepții enumerate anterior, cât și altele pe care tu le poți adăuga la rândul tău, vor extinde clasa Exception
.
Tratarea mai multor tipuri de excepții
Acum, ce vreau să știi e că poți prinde mai multe tipuri de excepții în același timp. Sau mai degrabă, nu în același timp, ci pe rând, pentru același bloc de try.
Există situații în care vrem să tratăm fiecare tip de excepție aruncată de o anume bucățică de cod, într-un mod diferit. Pentru că și motivul pentru aruncarea excepției este diferit pentru fiecare tip de excepție în parte.
Hai să vedem următorul exemplu:
try
{
Print("ceva text aici");
}
catch(ArgumentNullException)
{
Console.WriteLine("Te rog adaugă ceva text");
}
catch(ArgumentException ex)
{
Console.WriteLine($"Valoarea parametrului nu este în regulă: {ex.Message}, {ex.StackTrace}");
}
catch(Exception)
{
Console.WriteLine("S-a produs o eroare, te rog să reîncerci acțiunea");
}
static void Print(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentNullException("text este obligatoriu");
}
if (text.Length > 10)
{
throw new ArgumentException("text nu poate fi mai lung de 10 caractere");
}
}
După cum vezi, în cazul de față avem nevoie să tratăm individual fiecare excepție, pentru a ști ce mesaj să afișăm în consolă pentru utilizatorul final.
Prin definirea unui nume de parametru, în blocul de catch, așa cum e și cazul lui ArgumentException
putem citi mai multe detalii despre excepția care a fost prinsă, cât și stacktrace-ul, pentru a vedea exact cauza sau rădăcina problemei.
Spuneam mai devreme că Exception
e clasa de bază pentru toate celelalte tipuri de excepții din .NET. De-asta și poziția ei în enumerația de blocuri catch este ultima. Pentru că în cazul în care există o potențială excepție al cărei tip noi nu l-am știut, în afară de cele pe care deja le prindem, atunci aplicația noastră va rezulta într-o oprire bruscă.
Prinzând acolo și Exception
acoperim cazul tuturor excepțiilor existente.
Chiar dacă nu am știut să prindem tipul exact al excepției care ne-a fost aruncată, faptul că am prins Exception
a făcut ca aplicația noastră să nu cedeze și să gestioneze cum trebuie eroarea.
Tratarea excepțiilor se va face tot timpul de sus în jos pentru toate blocurile catch. Adică se va verifica întâi dacă codul nostru aruncă un ArgumentNullException
, apoi ArgumentException
, apoi Exception
.

Concluzie
Mecanismul de excepții este foarte bine înrădăcinat în majoritatea limbajelor de programare existente, într-o formă sau alta, indiferent de categoria limbajului.
În JavaScript, de exemplu, echivalentul excepțiilor sunt erorile. Orice situație neașteptată va produce un Error
în schimb, dar mecanismul prin care aceste erori se propagă este identic cu cel al excepțiilor.
Înțelegerea modului de funcționare al acestui mecanism e vital pentru a putea construi aplicații reziliente, dar în același timp ai nevoie de un pic de gândire critică pentru a le putea anticipa.
Cam atât pentru azi, pe data viitoare.