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:
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.
// BAD: Silent failure, impossible to debugtry { localStorage.setItem('user', JSON.stringify(user));} catch (e) { // Nothing happens - error is swallowed}What happens when this fails in production?
- User data isn’t saved
- No error appears in logs
- No user notification
- 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:
// HARMFUL: All errors disappearasync 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:
// UNNECESSARY in 2026: Safari 11+ handles this correctlyfunction 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:
- The code is unnecessary for modern browsers
- Even if the error occurred, the empty catch provides no protection
Harmful: No Recovery Path
An empty catch block provides no fallback:
// BAD: No fallback when localStorage failsfunction 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:
// Acceptable: Analytics failure shouldn't break the apptry { 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:
// Acceptable: Explicit feature detectionfunction 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:
// Acceptable: Cleanup operation that may failfunction 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
// GOOD: Handle the specific error casetry { 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:
// Better: Handle different error types appropriatelyclass 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:
// Production-ready error handlingfunction 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 visibility into what's happeningfunction savePreferences(prefs) { try { localStorage.setItem('prefs', JSON.stringify(prefs)); } catch (e) {}}With proper error handling:
// Now we can diagnose the issuefunction 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:
-
What specific errors am I expecting? If you can’t name them, you might be catching too broadly.
-
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)
-
Is this catch block handling or hiding? Handling means taking action. Hiding means silence.
-
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:
- 👨💻 MDN - try...catch Statement
- 👨💻 MDN - Web Storage API
- 👨💻 Does Safari Private Browsing Still Throw localStorage Errors?
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments