Skip to content

Empty Try/Catch Blocks: When Are They Actually Harmful vs Helpful?

A friend was reviewing my code and pointed at a block that looked like this:

harmful-empty-catch.js
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
// Silent failure
}

“That try/catch isn’t protecting against anything,” he said. “The catch block is empty. If Safari private browsing mode throws an error, you’re just swallowing it. No logging, no fallback, no nothing.”

I started to argue. “It’s defensive programming! Safari private mode…”

“Was fixed in 2017,” he cut me off. “And even if it wasn’t, an empty catch block isn’t handling anything. It’s just hiding the problem.”

He was right. I had to admit that my “defensive” try/catch wasn’t defensive at all. It was defensive theater—code that looks like error handling but provides no actual protection.

The Core Problem: Silent Failures

Empty catch blocks are the programming equivalent of a black hole. Errors go in, nothing comes out. When something goes wrong in production, you have no visibility into what happened.

silent-failure.js
// BAD: Silent failure, impossible to debug
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
// Nothing happens - error is swallowed
}

What happens when this fails in production?

  1. User data isn’t saved
  2. No error appears in logs
  3. No user notification
  4. No way to diagnose the issue

The user experiences broken functionality with zero explanation. The developer has no breadcrumbs to follow. The empty catch block has achieved nothing except hiding the evidence.

When Empty Catch Blocks Are Actually Harmful

Harmful: Swallowing Unexpected Errors

The most dangerous pattern is catching all errors and doing nothing with them:

swallowing-all-errors.js
// HARMFUL: All errors disappear
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (e) {
// Network error? Parse error? Auth error? Who knows!
}
}

This pattern masks:

  • Network failures
  • Authentication issues
  • API changes
  • Parse errors
  • CORS problems

All of these become invisible, making debugging nearly impossible.

Harmful: Based on Outdated Knowledge

My Safari localStorage example falls into this category. I was writing defensive code for a bug that was fixed in 2017:

outdated-workaround.js
// UNNECESSARY in 2026: Safari 11+ handles this correctly
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
// This was meant to catch Safari private mode errors
// But Safari fixed this in 2017
}
}

This is doubly problematic:

  1. The code is unnecessary for modern browsers
  2. Even if the error occurred, the empty catch provides no protection

Harmful: No Recovery Path

An empty catch block provides no fallback:

no-recovery.js
// BAD: No fallback when localStorage fails
function savePreferences(prefs) {
try {
localStorage.setItem('prefs', JSON.stringify(prefs));
} catch (e) {
// User's preferences are just... gone
// No fallback to cookies, session, or memory
}
}

If localStorage is unavailable (quota exceeded, disabled, etc.), the user’s preferences are lost with no alternative.

When Empty Catch Blocks Might Be Acceptable

There are limited scenarios where an empty catch block is defensible, but they require careful consideration.

Acceptable: Truly Optional Enhancements

For operations that genuinely shouldn’t affect user experience if they fail:

optional-enhancement.js
// Acceptable: Analytics failure shouldn't break the app
try {
trackEvent('page_view', { referrer: document.referrer });
} catch (e) {
// Analytics failure shouldn't affect user experience
// But consider at least: if (DEBUG) console.warn('Analytics failed:', e);
}

Even here, I’d argue for at least a debug-mode warning. Complete silence makes debugging harder.

Acceptable: Feature Detection

When you’re explicitly testing if something is possible:

feature-detection.js
// Acceptable: Explicit feature detection
function supportsLocalStorage() {
try {
localStorage.setItem('__test__', '1');
localStorage.removeItem('__test__');
return true;
} catch (e) {
return false;
}
}

This is intentional—the function’s purpose is to check availability, so returning false on any error is the correct behavior.

Acceptable: Expected Failures With No Impact

When failure is expected and has no user-facing consequence:

expected-failure.js
// Acceptable: Cleanup operation that may fail
function cleanupOldSession() {
try {
sessionStorage.removeItem('legacy_key');
} catch (e) {
// If this fails, it doesn't matter—the session is old anyway
}
}

Proper Error Handling Patterns

Instead of empty catch blocks, implement proper error handling.

Pattern 1: Log and Provide Fallback

log-and-fallback.js
// GOOD: Handle the specific error case
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
// Storage full - clear old data and retry
localStorage.clear();
localStorage.setItem('user', JSON.stringify(user));
} else {
// Log unexpected errors
console.error('localStorage unavailable:', e);
// Fallback to in-memory storage
memoryStorage.user = user;
}
}

This pattern:

  • Handles the expected error case (quota exceeded)
  • Logs unexpected errors for debugging
  • Provides a fallback that keeps the app functional

Pattern 2: Explicit Error Types

Create and handle specific error types for better error management:

explicit-errors.js
// Better: Handle different error types appropriately
class ApiError extends Error {
constructor(status) {
super(`API error: ${status}`);
this.status = status;
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
class AuthError extends Error {
constructor(message) {
super(message);
this.name = 'AuthError';
}
}
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new ApiError(response.status);
return await response.json();
} catch (e) {
if (e instanceof ApiError) {
if (e.status === 404) return null; // User not found
if (e.status === 401) throw new AuthError('Please log in');
}
// Unexpected errors should propagate or be logged
logger.error('Failed to fetch user', { userId, error: e });
throw new NetworkError('Unable to load user data');
}
}

This approach:

  • Distinguishes between different failure modes
  • Allows callers to handle specific errors
  • Maintains visibility through logging
  • Provides meaningful error messages

Pattern 3: Structured Logging

For production applications, use structured logging instead of console.error:

structured-logging.js
// Production-ready error handling
function logError(context, error) {
// Send to error tracking service
errorTracker.captureException(error, {
tags: { context },
extra: {
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
}
});
// Also log in development
if (process.env.NODE_ENV === 'development') {
console.error(`[${context}]`, error);
}
}
try {
localStorage.setItem('session', JSON.stringify(session));
} catch (e) {
logError('localStorage', e);
// Provide user feedback for critical operations
showNotification('Unable to save session. Some features may be limited.');
}

The Debugging Nightmare

Let me illustrate why empty catch blocks are so harmful with a real scenario.

Imagine a user reports: “My preferences keep resetting.”

Without proper error handling:

no-debug-info.js
// No visibility into what's happening
function savePreferences(prefs) {
try {
localStorage.setItem('prefs', JSON.stringify(prefs));
} catch (e) {}
}

With proper error handling:

with-debug-info.js
// Now we can diagnose the issue
function savePreferences(prefs) {
try {
localStorage.setItem('prefs', JSON.stringify(prefs));
} catch (e) {
logError('savePreferences', e);
// After checking logs, we discover:
// "QuotaExceededError: Storage quota exceeded"
// User had filled localStorage with cached images
// Now we can fix the root cause
clearOldestCache();
localStorage.setItem('prefs', JSON.stringify(prefs));
}
}

The second version gives you:

  • Visibility into the error
  • The specific error type
  • A path to fixing the root cause

A Decision Framework

When writing a try/catch block, ask yourself:

  1. What specific errors am I expecting? If you can’t name them, you might be catching too broadly.

  2. What should happen when an error occurs?

    • Log it? (For debugging)
    • Notify the user? (If it affects their experience)
    • Provide a fallback? (If the feature is critical)
    • Propagate it? (If the caller should handle it)
  3. Is this catch block handling or hiding? Handling means taking action. Hiding means silence.

  4. Would I know this error occurred in production? If the answer is no, add logging.

The Bottom Line

Empty try/catch blocks are defensive theater. They provide an illusion of safety while creating real problems:

  • Silent failures that are impossible to debug
  • False confidence that your code is handling errors
  • Hidden bugs that surface only in production
  • Wasted time tracking down issues that should have been logged

The correct approach is simple: if you’re going to catch an error, do something with it. Log it, handle it, or let it fail loudly. An empty catch block does nothing except make debugging harder.

After that code review conversation with my friend, I went through our codebase and found 47 empty catch blocks. Each one was a potential debugging nightmare waiting to happen. I replaced them all with proper error handling—some with logging, some with fallbacks, some with user notifications.

The next time a user reported an issue, I had logs to look at. The debugging nightmare was over.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments