When a .NET program encounters a situation it cannot handle — a file that does not exist, a division by zero, a null reference — it throws an exception. An exception is an object that carries information about the error and unwinds the call stack until a catch block handles it. If no catch block is found, the program terminates. In ASP.NET Core, a global exception handler catches anything that escapes your code, so you can focus on handling errors you know about and letting the framework handle the rest.
Try / Catch / Finally
// ── Basic try/catch ────────────────────────────────────────────────────────
try
{
int result = Divide(10, 0); // throws DivideByZeroException
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Cannot divide by zero: {ex.Message}");
}
// ── Multiple catch blocks — most specific first ───────────────────────────
try
{
string? input = Console.ReadLine();
int parsed = int.Parse(input!); // throws FormatException if not a number
int result = 100 / parsed; // throws DivideByZeroException if 0
Console.WriteLine(result);
}
catch (FormatException ex)
{
Console.WriteLine($"Not a valid number: {ex.Message}");
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero.");
}
catch (Exception ex) // general catch — always last
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
// ── Finally — always runs, even if an exception occurs ────────────────────
StreamReader? reader = null;
try
{
reader = new StreamReader("data.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (FileNotFoundException)
{
Console.WriteLine("File not found.");
}
finally
{
reader?.Dispose(); // always runs — cleanup guaranteed
Console.WriteLine("Done processing file.");
}
finally block runs in every case: when the try block completes normally, when an exception is caught by a catch block, and when an exception is NOT caught and is propagating up the stack. The only exception (pun intended) is a StackOverflowException or an environment-level kill that terminates the process. In practice, finally is mainly used for resource cleanup — but in modern C#, using statements and IDisposable handle this more cleanly than explicit finally blocks.using statement over try/finally for resource cleanup. using var reader = new StreamReader("data.txt") automatically calls Dispose() when the variable goes out of scope — equivalent to a try/finally but far more concise. For async resources, use await using. The using pattern is the idiomatic C# approach and should replace explicit finally { resource.Dispose() } in virtually all cases.catch (Exception ex) { throw ex; } to rethrow — this destroys the original stack trace, making debugging nearly impossible. The correct rethrow is simply throw; (no argument), which preserves the original exception and its full stack trace. If you need to wrap the exception in a new one, use: throw new ApplicationException("Context message", ex) — the original exception becomes the InnerException and is fully preserved.Rethrowing and Wrapping Exceptions
// ❌ Wrong — destroys the original stack trace
catch (Exception ex) { throw ex; }
// ✅ Correct rethrow — preserves full stack trace
catch (Exception ex)
{
_logger.LogError(ex, "Error in ProcessAsync");
throw; // rethrows the original exception unchanged
}
// ✅ Wrap in a more descriptive exception
catch (SqlException ex)
{
throw new DataAccessException(
$"Database error loading post {postId}",
innerException: ex); // original is preserved as InnerException
}
Exception Properties
| Property | Type | Description |
|---|---|---|
Message |
string | Human-readable error description |
StackTrace |
string? | Call stack at time of throw |
InnerException |
Exception? | The exception that caused this one |
Source |
string? | Assembly or object that threw |
HResult |
int | HRESULT error code (COM interop) |
Data |
IDictionary | Key-value pairs of additional data |
Common Mistakes
Mistake 1 — Catching general Exception when specific types are available
❌ Wrong — catches everything including OutOfMemoryException and StackOverflowException:
catch (Exception) { /* silently swallowed */ }
✅ Correct — catch the specific exceptions you can handle, let others propagate.
Mistake 2 — Empty catch block (exception swallowing)
❌ Wrong — error silently disappears, leaving the application in an unknown state:
try { DoWork(); } catch { } // never do this
✅ Correct — at minimum log the exception before rethrowing or returning an error result.