Have you ever looked at code like this and thought:
“Wait… this looks incomplete.
If there’s nocatch, why is there atryat all?”
try {
const accessGranted = testSilentDoor(door)
updateStatusPanel(!accessGranted)
} finally {
hallwayInUse.current = false
}
I’ve thought that too.
For a long time, this pattern felt strange, redundant, or simply wrong.
That changed after I ran into a silent production bug — the kind that doesn’t crash anything, doesn’t log an error, and yet makes part of the system stop working for no obvious reason.
That’s when try / finally finally clicked.
The Most Common Misunderstanding
Most of us are taught something like this:
-
try→ attempt something -
catch→ handle the error -
finally→ runs at the end
From that, it’s easy (and very common) to conclude:
“If there’s no
catch, thetryis useless.”
❌ That conclusion is wrong.
In practice:
catchhandles errorsfinallyguarantees state
These are orthogonal responsibilities.
The Bug That Made Everything Make Sense
Consider a simplified flow like this:
if (hallwayInUse.current) return
hallwayInUse.current = true
const accessGranted = testSilentDoor(door)
updateStatusPanel(!accessGranted)
hallwayInUse.current = false
It looks fine… until one day:
- an exception is thrown
- a promise rejects
- someone adds a
returnin the middle - or a rare failure happens
Result?
A shared flag is never restored, a lock is never released, and the system enters a logical deadlock.
No crash.
No clear error.
Just… stuck.
These bugs are brutal to diagnose.
What finally Is Actually Doing
Now look at the same logic written this way:
hallwayInUse.current = true
try {
const accessGranted = testSilentDoor(door)
updateStatusPanel(!accessGranted)
} finally {
hallwayInUse.current = false
}
This code is not trying to handle errors.
It’s declaring an explicit contract:
“While this operation is running, a shared resource is locked.
When it finishes — no matter how — that resource must be released.”
That’s the key idea.
“But Wouldn’t It Be the Same After the try?”
No — and this difference matters a lot.
With finally, cleanup logic runs even when:
- a
returnhappens - an exception is thrown
- an async operation rejects
# with finally
function patrol() {
try {
return
} finally {
releaseHallway()
}
}
Without finally, cleanup is best-effort, not guaranteed.
# without finally
function patrol() {
try {
return
}
releaseHallway()
}
❌ releaseHallway() never runs.
The same is true for:
throw
async errors
rejected promises
unexpected exceptions
The Analogy That Made It Obvious
Think of this flow as a door with an automatic loc:
lockDoor()
try {
enterRestrictedRoom()
inspectDashboard()
} finally {
unlockDoor()
}
You lock the door, enter a restricted room, do your work… and then leave.
The question is simple:
Should the door unlock only if everything succeeds?
Of course not.
It must unlock:
- on success
- on failure
- halfway through
- even if something unexpected happens
The error isn’t the door’s concern.
The door’s job is not locking down the entire building.
That’s exactly what finally is for.
When Using try Without catch Is the Right Choice
Use this pattern when you must guarantee cleanup or state restoration, such as:
- releasing a lock
- resetting a flag
- stopping a spinner
- restoring temporary state
- preventing logical deadlocks
Classic examples:
setOperationActive(true)
try {
runTask()
} finally {
setOperationActive(false)
}
occupyHallway()
try {
crossCriticalZone()
} finally {
releaseHallway()
}
When You Should NOT Do This
Do not rely only on finally when you need to:
- show an error to the user
- decide business flow
- apply a fallback
- transform or interpret an exception
In those cases, a catch is required.
A Simple Mental Rule (Worth Remembering)
catchhandles failures.
finallyprotects invariants.
One does not replace the other.
If something must be undone, use finally.
If something must be handled, use catch.
Sometimes you need both.
Sometimes only one.
Closing Thoughts
This pattern:
try {
// main logic
} finally {
// mandatory cleanup
}
“try something, but always restore state afterward” —
is not weird, incomplete, or careless.
It’s defensive, explicit, and mature code.
Once you start seeing finally as a guarantee of state, rather than “the place where errors go”, it stops being confusing — and starts protecting your system from subtle, expensive bugs.
If this article prevents even one silent logical deadlock in production, it did its job.


Top comments (0)