A try block runs some code that might throw. A catch block handles the throw. A finally block is the third piece, and it has a special promise: the code inside finally runs whether the try succeeded, whether the catch ran, or whether an exception escaped the whole statement.
finally sits after try and catch, and it always runs:
try {
// work that might throw
} catch (SomeException e) {
// respond to the failure
} finally {
// cleanup, logging, or anything that must always happen
}
The cleanup runs after the try body returns normally, after catch handles a thrown exception, and even when the catch itself rethrows or throws a different exception that escapes the whole statement. Code after the try-catch-finally will not run when an exception escapes, but the finally block still runs.
The classic use is releasing a resource: closing a database connection, releasing a lock, or flushing a buffer. Putting that work in finally guarantees it happens even if an exception interrupts the flow. Logging the end of an operation belongs in finally for the same reason: a log entry on the way out is more useful than one that gets skipped because something threw halfway through.
try {
System.debug('Starting work');
doRiskyWork();
System.debug('Finished work');
} catch (Exception e) {
System.debug('Failed: ' + e.getMessage());
} finally {
System.debug('Cleanup runs every time');
}
If doRiskyWork() succeeds, both the success log and the cleanup log print. If it throws, the failure log prints, then the cleanup log prints. The cleanup is the only line guaranteed to run on every path.
Putting cleanup at the END of the try block is the most common slip. That works when nothing throws, but the cleanup is skipped the first time the catch handler is needed. The whole point of finally is to move the work outside both branches so it runs no matter which one was taken.
A return statement inside try does not skip finally either: the finally block runs before the method actually returns. So finally is the right place to release anything the method was holding, even when the method is about to return normally.
Cleanup code that runs only on the happy path is a bug waiting to happen: the first time the catch path is exercised, the connection stays open, the counter does not increment, the log line goes missing. Putting the work in finally is the one place in Apex where the language guarantees it will run on every exit path.