DEV Community

Cover image for Why I Use try with finally Without catch (and Why This Isn’t “Weird Code”)
Werliton Silva
Werliton Silva

Posted on

Why I Use try with finally Without catch (and Why This Isn’t “Weird Code”)

Have you ever looked at code like this and thought:

finally

“Wait… this looks incomplete.

If there’s no catch, why is there a try at all?”

try {
  const accessGranted = testSilentDoor(door)
  updateStatusPanel(!accessGranted)
} finally {
  hallwayInUse.current = false
}
Enter fullscreen mode Exit fullscreen mode

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, the try is useless.”

❌ That conclusion is wrong.

In practice:

  • catch handles errors
  • finally guarantees 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
Enter fullscreen mode Exit fullscreen mode

It looks fine… until one day:

  • an exception is thrown
  • a promise rejects
  • someone adds a return in 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
}
Enter fullscreen mode Exit fullscreen mode

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 return happens
  • an exception is thrown
  • an async operation rejects
# with finally
function patrol() {
  try {
    return
  } finally {
    releaseHallway()
  }
}
Enter fullscreen mode Exit fullscreen mode

Without finally, cleanup is best-effort, not guaranteed.

# without finally

function patrol() {
  try {
    return
  }

  releaseHallway()
}
Enter fullscreen mode Exit fullscreen mode

❌ 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()
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
occupyHallway()
try {
  crossCriticalZone()
} finally {
  releaseHallway()
}
Enter fullscreen mode Exit fullscreen mode

When You Should NOT Do This

notdothis

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)

catch handles failures.

finally protects 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
}
Enter fullscreen mode Exit fullscreen mode

“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)