Nino Nađ

Notes

Avoiding oversized catch blocks

Narrow catch blocks make rollback boundaries easier to reason about and reduce accidental coupling.

Error handling Code review Backend

One small thing I pushed on in a recent PR review was error handling in service code.

The flow looked reasonable at first. Create the document, upload the file, and if something fails, clean up in catch.

Something like this:

try {
  await createDocument();
  await uploadFile();
} catch (error) {
  await deleteDocument();
  throw error;
}

The issue is not the cleanup itself. The issue is the size of the catch.

A big catch quietly defines a rollback boundary. Today that boundary might only mean “document + file upload”. But later someone adds one more line inside the same try: create another relation, call another service, write metadata, trigger some side effect. Now an unrelated exception can delete the whole document even though that was never really the intent.

That is the bad pattern I try to avoid: error handling that is correct for today’s code, but too broad for tomorrow’s code.

My preference is simple. If I know exactly which step can fail and what state it owns, I want the throw and the cleanup as close to that step as possible. Small catch blocks are easier to reason about. Big ones create hidden coupling.

There is usually another smell next to this too: turning internal failures into validation errors. If the upload layer, database, or network fails, that is not a client mistake. The code should preserve that signal instead of hiding it behind a generic validation error.

So the rule I try to follow is straightforward: catch narrowly, clean up narrowly, and do not delete more state than the failing step actually owns.

Maybe the broader cleanup is fine for the current requirement. Maybe nobody ever adds more logic there. But I would still rather separate that responsibility now than come back later and untangle a misleading rollback path.