Async .NET · Lesson 7

Finally and ownership

Run cleanup after asynchronous success or failure.

Cleanup belongs to an explicit owned scope

An operation may need cleanup whether its main work succeeds or fails. A finally block expresses that obligation for ordinary managed control flow, while a catch block decides how to handle a particular failure. The sample keeps those responsibilities separate: the catch reports a known InvalidOperationException and finally prints a cleanup marker. Recovery and cleanup should not be confused. A failure can be reported successfully while resources still need releasing, and resources can be released even when the failure must continue propagating to an outer owner.

Follow try, catch, and finally in order

The try body first awaits the small delay and then deliberately throws InvalidOperationException. The matching catch prints failed. After that handler finishes, finally prints cleanup, producing those two lines in that order. The cleanup marker is not evidence that the attempted work succeeded; it shows that the scoped cleanup path ran after the known failure was handled. No retry occurs, no successful result is substituted, and the timer duration does not determine the ordering of these two output statements.

finally { Console.WriteLine("cleanup"); }

Await does not erase the surrounding control structure

The try/catch/finally structure spans the await. If a failure is observed at an awaited operation or thrown by later statements in the same try, the ordinary exception-handling rules still determine the applicable handler and cleanup path. Async methods preserve those logical boundaries while suspension and continuation are managed by the compiler. That lets a resource owner place cleanup next to acquisition and use, instead of attempting to remember every possible return or exception point manually throughout the workflow.

Cleanup can fail and must match ownership

A finally block should perform the cleanup the scope actually owns. Closing a borrowed resource can break another caller, while omitting an owned resource can leave a leak. Cleanup code can itself throw, potentially obscuring an earlier failure if the design does not account for both. Keep cleanup focused and use using or await using for resources with the corresponding disposal contract. Ordinary finally behavior also should not be generalized into a guarantee that cleanup runs after a host forcibly terminates execution.

Practice: compare success and an unhandled failure

Remove the deliberate throw and add a success marker after the delay. Predict success followed by cleanup. Then restore the failure but change it to an exception type not handled by the current catch, and observe the cleanup marker alongside the outer reported failure. Restore the original runnable sample afterward. These variations distinguish a matching catch, no failure, and a propagated failure while keeping the cleanup obligation attached to the same scope.

  • Write expected marker order for each path before running it.
  • Do not add a broad catch merely to make every experiment look like the baseline’s handled failure.

Practice: replace the marker with a visible resource lifetime

For an independent exercise, introduce a small IDisposable lease whose Dispose prints a release marker and acquire it with using inside an owned scope. Compare that lifetime with the hand-written finally marker, including a controlled failure during use. Then consider an IAsyncDisposable lease and why await using is required if cleanup itself must complete asynchronously. Keep the exercise local and simulated so its result demonstrates ownership rather than depending on a real file, socket, or native platform resource.

  • A borrowed resource should have a documented owner instead of being disposed by every method that receives it.
  • For asynchronous cleanup, wait for its completion before reporting that the owning operation is finished.

Describe both the outcome and the release obligation

The baseline’s useful conclusion is that the known failure is reported and the cleanup path then executes. It does not claim that every exception is recoverable or that a cleanup marker can replace a real disposal contract. In larger operations, list the resources acquired, the scope that owns each one, and the behavior if acquisition, use, or cleanup fails. Clear ownership is what makes finally, using, and await using reliable tools rather than decorative syntax around an otherwise ambiguous resource lifetime.

  • Baseline output is failed, then cleanup.
  • Catch selects a failure-handling path; finally represents a separate scoped cleanup obligation.
  • Using and await using can express disposal ownership more directly when the resource supports those contracts.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

try
{
    await Task.Delay(1);
    throw new InvalidOperationException();
}
catch (InvalidOperationException) { Console.WriteLine("failed"); }
finally { Console.WriteLine("cleanup"); }

Expected output

failed
cleanup
Try this
Cleanup follows the ownership boundary even across an await.

Further reading