Try, Catch and Finally — The Exception Handling Fundamentals

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.");
}
Note: The 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.
Tip: Prefer the 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.
Warning: Never write 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.

🧠 Test Yourself

You have a try/catch/finally block. The try throws an exception that the catch handles. Does the finally block still run?