Last Updated: May 22, 2026
A finally block is the part of a try statement that runs no matter what happens above it. The try body might complete normally, throw an exception that a catch clause handles, or throw an exception that nothing catches and that propagates out of the method. In every one of those paths, the code in finally still runs before control leaves the statement, which makes it the place for cleanup work that must happen regardless of outcome.
The point of finally is a guarantee, not a convenience. Cleanup code that runs only on the happy path isn't cleanup, it's a wish. If you open a file, you have to close it whether the rest of the method succeeded or failed. If you reserve five units of inventory while preparing a checkout, you have to release that reservation if anything goes wrong later. If you take a lock, you have to release it on the way out, no matter how that way out happens. A try/finally is the language-level mechanism that makes "this runs on the way out, period" something the compiler enforces.
There's no exception here at all, and the finally still runs. That's the first path: the try completes normally, control falls off the end of the block, and the runtime takes a short detour through finally before continuing past the statement. The block isn't tied to "something went wrong"; it's tied to "we're leaving this try."
Now add an exception that nothing handles, and watch the same finally fire on the way out:
The inner try has no catch. The throw immediately starts unwinding, but before control leaves the inner try statement, the runtime executes the finally. Only after the cleanup line prints does the exception continue propagating up to the outer catch, which finally handles it. The order is fixed by the language: cleanup first, then propagation continues.
A third path. Catch the exception inside the same try and watch the order in which catch and finally fire:
The catch ran first because the exception matched. Then finally ran on the way out of the statement. After both, control fell through to whatever came next (here, the end of Main). The finally doesn't care whether catch caught something, whether catch rethrew, or whether catch ran at all. It only cares that the try is being left.
The guarantee has narrow exceptions that you should know exist but shouldn't design around. If the process is torn down hard (the OS kills it, the machine loses power, someone calls Environment.FailFast), finally blocks don't run, because there's no runtime left to run them. A StackOverflowException bypasses finally for the same reason: the stack the runtime would need to invoke the block isn't available. An ExecutionEngineException (rare in modern .NET) is a similar "runtime is broken" situation. Outside of these corner cases, finally runs. Treat it as a hard promise for cleanup logic and stop worrying about the edge cases unless you're writing a host process.
Every path through the diagram passes through the finally node. The two outcomes after finally are "continue normally" (when the exception was caught or there was no exception) and "keep unwinding" (when the exception wasn't matched by any catch). Both paths run finally first.
The order of operations inside a try statement is fixed. The runtime starts at the top of the try block. If the block runs to completion without throwing, the runtime then runs finally (if there is one) and proceeds to whatever follows. If the try body throws, the runtime walks the list of catch clauses in source order, looking for the first one whose declared type matches the thrown exception. If it finds a match, that catch runs to completion, then finally runs. If no catch matches, finally still runs, but the exception continues unwinding rather than being suppressed.
A small program that prints which stage runs makes this concrete:
The first call runs the whole try, skips the catch (nothing was thrown), and ends with finally. The second call throws partway through the try, jumps to the matching catch, runs it, and ends with finally. The line try: end doesn't appear in the second run because the throw short-circuited the rest of the block. That's not specific to finally, it's how throw always works. The rest of the try body does not "resume" after a caught exception.
Now stack two try/finally blocks and watch the order when the inner one rethrows:
The exception leaves the inner try. Before it does, the inner finally runs. The exception then walks up to the outer try, finds a matching catch, runs it, and finally runs the outer finally. finally blocks always run innermost first, on the way out, in the order their try statements are nested. This makes finally safe for paired cleanup: an outer block can rely on inner blocks having already released their resources before the outer block's finally runs.
A try block with finally adds a small bookkeeping cost in the prologue and epilogue of the method, because the runtime has to register the handler so it knows where to jump on an exception. It's not measurable in normal code. Don't avoid finally for performance.
A diagram of the order, with two nested try statements:
The inner finally runs before any outer catch even gets a chance to look at the exception. That's the rule for nested handlers: cleanup at every scope runs on the way past, in order, from the deepest scope outward.
A finally block isn't only triggered by exceptions. It's also triggered by any other way of leaving the try body early: return, break, continue, or goto out of the block. The language guarantee is broader than "runs on exception"; it's "runs whenever control leaves the try."
return inside try:
The return 1 evaluates the return value, but control doesn't actually leave ProcessCheckout yet. The runtime first runs finally, and only after that does it return to the caller with the value 1. The order on screen confirms it: Releasing reservation prints before Returned: 1. If finally didn't run on return, the reservation would leak every time the method returned successfully, which would defeat the point of finally entirely.
The same applies to break and continue inside a loop:
The continue on the second iteration skips the rest of the try body and jumps to the next iteration of the loop, but on the way out of the try, finally still runs. The release line prints for order 2 just like it does for the orders that didn't continue. The same is true if you swap continue for break: the loop ends, but finally runs first.
What about evaluation order when the return expression itself does work? The expression is evaluated inside the try, the result is held, finally runs, and then the held result is returned. If the expression has side effects, those happen at expression-evaluation time:
Reserve() runs first because it's the expression being returned. The result 1 is captured. Then finally runs and observes Counter = 1. Then the captured value is returned to Main. The finally block sees the world after the return expression evaluated, but before control actually leaves the method.
There's a subtlety with mutable state. If the return value is a reference type, and finally mutates the object the reference points at, the caller sees the mutation, because both finally and the caller are looking at the same object. The return value of a try is the value that was captured at the return statement, but if that value is a reference, the thing it references can still be changed by finally before the caller gets a chance to use it. With value types, the captured copy is what the caller receives, and finally can't change it after the fact.
Build returns the items reference. finally then adds an item to that same list before control leaves the method. The caller receives the list and finds both entries, because the captured return value is the reference and the underlying object kept changing right up until the method exited. This is rarely what you want, and finally should generally avoid mutating things the return value points at.
A finally block is regular code. Nothing stops it from throwing an exception of its own. When it does, the exception that was already in flight (if there was one) gets discarded, and the new exception takes its place. The original is gone. No inner-exception chain, no aggregate, no second chance. The runtime simply replaces the in-flight exception with the new one and continues unwinding.
The original InvalidOperationException is nowhere to be found. The outer catch sees only the replacement, and there's no link back to the exception that was unwinding when the finally threw. That's the most dangerous behavior in this lesson: a buggy finally can erase the real reason a method failed and leave the caller debugging the wrong problem.
The fix: don't let finally throw. Any code in finally that could plausibly throw should be wrapped in its own try/catch, and the catch should log or swallow rather than rethrow. The contract you're aiming for is "cleanup attempts to run, doesn't crash, and never replaces the in-flight exception."
The cleanup attempt failed, and the inner catch inside the finally logged it without rethrowing. The original "Payment declined" exception kept its place in flight and reached the outer catch, where it can be diagnosed properly. The log line tells you cleanup didn't go well, so you don't silently lose that information either. Both signals survive, the primary failure isn't masked.
This is one of the reasons production code prefers using (which expands to try/finally with careful cleanup) and helper methods that promise not to throw on disposal. A Dispose method that throws is considered a bug in most .NET guidance, precisely because it can mask other failures.
Wrapping cleanup in an inner try/catch adds another small handler registration. It's negligible. Pay it whenever cleanup could throw, because the alternative (losing the real exception) is much worse than a fraction of a microsecond.
A diagram of the swap, since the loss is easy to miss when reading code:
The replacement is total. There's no language facility that preserves the original automatically. If you want both, you have to wire it up yourself: catch the cleanup failure inside finally, log it, and let the original continue. The snippet above shows the pattern.
You don't need a catch clause to use finally. A bare try/finally is a legal and common pattern, and it expresses something specific: "I have cleanup to run, but I don't want to handle any exception that happens here. Let it propagate to whoever does."
ProcessCheckout has no idea how to handle a card decline. That's a concern for the caller, not for the checkout method. What ProcessCheckout does care about is releasing the inventory reservation it took, regardless of whether the charge worked. The try/finally expresses that: do the cleanup, then let whatever happened propagate. The decline reaches Main, where there's an actual policy for what to do with it.
This pattern shows up in many places in C#. Database transactions wrap their work in try/finally so the connection returns to the pool even on failure. File operations wrap their reads in try/finally so the handle closes regardless of what the parser found. Locks wrap their critical sections so the lock releases whether the body completed cleanly or threw. None of these finally blocks have any business "handling" an exception; their job is cleanup.
Two ways of writing the same cleanup, one with a swallowing catch and one with a bare finally:
BadVersion swallows the exception by catching it without rethrowing. The caller never learns the charge failed, because as far as it can tell the method completed normally. That's almost always incorrect. GoodVersion runs the same cleanup but lets the exception propagate. The caller knows something went wrong and can decide what to do.
The line Console.WriteLine("Continued past try"); highlights the difference. In BadVersion, the swallowing catch consumed the exception and execution continued past the try statement, so the line prints. In GoodVersion, the exception is still in flight after finally, so the line never executes; control jumps straight to the caller.
A bare try/finally is also the appropriate shape when there's no meaningful handling you could do at this layer. If the only thing you'd write in a catch is throw;, then there's no catch worth writing. Use try/finally and skip the noise.
If the cleanup you're protecting is the disposal of an object that implements IDisposable, C# has a shorter form for the same idea. The using statement is shorthand for try/finally plus a Dispose call, so anywhere you'd write try/finally purely to call Dispose, using says the same thing in less code.
Both versions do the same thing. The compiler turns the using statement into the equivalent try/finally, and the Dispose call runs on the way out regardless of whether the body completed normally or threw. The using form is less code.
Memory management and IDisposable have their own section later in the course, where the patterns (using declaration in C# 8+, await using for IAsyncDisposable, custom disposable types) get the space they deserve. For this lesson, the relationship matters: using is try/finally plus a disposal call, optimized for the common case. When the cleanup is something other than disposal (releasing a non-disposable reservation, restoring a global setting, logging a counter), use try/finally. When the cleanup is Dispose, using is shorter.
using and the equivalent try/finally produce nearly identical IL. There's no performance difference worth considering. Pick whichever reads better for the cleanup you have.
A comparison table:
| Situation | Tool |
|---|---|
Cleanup is Dispose on a single object | using |
Cleanup is Dispose on several objects | Multiple using declarations, or one nested using block |
| Cleanup is releasing a reservation, restoring state, or logging | try/finally |
| Cleanup needs to inspect the state of an exception | try/catch/finally |
Cleanup is async (DisposeAsync) | await using |
using doesn't replace try/finally. using fits one specific shape of cleanup, and try/finally covers everything else.
There's a short list of things finally blocks aren't allowed to do. The main one: you can't return from inside a finally block. The compiler rejects it with error CS0157, "Control cannot leave the body of a finally clause."
The error makes sense once you think about what return from finally would mean. The try returned 1. The finally ran. If finally then said "actually, return 2," would it override the original return? What about an exception that was unwinding through finally? If finally returned a value, the exception would be silently swallowed and the return would take over. That's a hazard the language designers chose to close at compile time rather than leave open at runtime.
The same restriction applies to break, continue, and goto that would take control out of the finally block. They're all rejected:
A break inside finally would let cleanup change the control flow of the surrounding loop, which is the same kind of hazard as return. Reject at compile time, same error.
What you can do is throw from inside finally. That's allowed (and as we saw earlier, dangerous, because it replaces the in-flight exception). Loops and gotos that stay inside the finally block are fine. The restriction is specifically about control leaving the finally.
The loop inside finally is fine. Control isn't leaving the finally block, it's iterating inside it. The restriction is on break, continue, return, and goto whose target lies outside the finally.
Two other edges exist. The narrow exceptions to the "finally always runs" guarantee (the ones mentioned at the top of this lesson) are real: StackOverflowException and process tear-down can skip finally. Don't put critical-to-the-process cleanup in finally and assume it can never be missed. For most application code, those edges don't matter; for hosts and runtimes, they do. The other edge is asynchronous exceptions like ThreadAbortException on .NET Framework, which had unusual semantics with finally. .NET 5 and later don't support thread abort, so this isn't a concern for modern code.
What's wrong with this code?
The finally block tries to return a value, which the compiler rejects with CS0157. Even setting aside the compile error, the intent is wrong: if the try returned the actual first line, the finally would silently overwrite it with "no line". The fix is to move the fallback out of finally and put the cleanup (closing the reader) there instead:
Fix:
Now finally does only cleanup. The return value comes from the try, the reader gets disposed on every path, and the compiler is happy.
The pattern try/finally enforces fits a specific shape: a resource is acquired, used, and released. The acquisition has to happen before the try, because if the acquisition fails, there's nothing to release. The use happens inside the try body. The release happens in finally, where it runs whether the use succeeded or threw.
The two arrows out of InUse show the two paths the lifecycle can take, and they both land at Released. That's the property finally guarantees: no path through the lifecycle skips the release step.
A concrete checkout example pulls these threads together. The method reserves inventory, attempts to charge, and releases the reservation if anything goes wrong before the charge completes:
In the successful run, the charge worked, charged was set to true, and finally saw that and left the reservation alone (because we want to keep the inventory committed for an order that's actually paid for). In the failing run, the charge threw, charged stayed false, and finally released the reservation before letting the exception propagate. The caller sees "Card declined" in both cases, and the inventory state is consistent in both cases.
A boolean flag like charged to remember whether the work completed is common when the cleanup behavior depends on what happened. finally runs unconditionally, so the conditional logic about what to do lives inside finally. The flag is the bridge between the try body and the cleanup logic.