AI is redefining software quality and security. Insights from 450 CISOs & devs →
Aikido

Handle errors in catch blocks: why empty catch blocks break production systems

Rule
Handle errors in catch blocks. 
Empty catch blocks silently swallow errors, making debugging difficult. 
Supported languages: Java, C, C++, PHP, JavaScript, TypeScript, Go, Python

Introduction

Empty catch blocks are one of the most dangerous anti-patterns in production code. When exceptions are caught but not handled, the error disappears without a trace. The application continues running with corrupted state, invalid data, or failed operations that should have stopped execution. Users see silent failures where features don't work but receive no error messages. Operations teams have no logs to debug with. The only indication something is wrong comes hours or days later when cascading failures make the system unusable.

Why it matters

Debugging and incident response: Empty catch blocks eliminate error logs. Engineers have no stack trace, error message, or indication of when or where the failure occurred, making issues nearly impossible to reproduce.

Silent data corruption: When database operations or API calls fail inside empty catch blocks, the application continues as if they succeeded. Records are partially updated, transactions are incomplete, and by the time corruption is discovered, the audit trail is gone.

Security vulnerabilities: Empty catch blocks mask security failures like authentication errors or authorization checks. An attacker triggering an exception in a security-critical path might bypass protections entirely if the error is silently swallowed.

Cascading failures: When errors are swallowed, the application continues in an invalid state. Subsequent operations depending on the failed operation's result will also fail, creating a chain of failures that misleads engineers from the actual root cause.

Code examples

❌ Non-compliant:

async function updateUserProfile(userId, profileData) {
    try {
        await db.users.update(userId, profileData);
        await cache.invalidate(`user:${userId}`);
        await searchIndex.update(userId, profileData);
    } catch (error) {
        // TODO: handle error
    }

    return { success: true };
}

Why it's wrong: If any operation fails, the error is silently ignored and the function returns success. The database might be updated but cache invalidation could fail, leaving stale data. Or the search index update fails, making the user unsearchable, with no log or alert to indicate the problem.

✅ Compliant:

async function updateUserProfile(userId, profileData) {
    try {
        await db.users.update(userId, profileData);
        await cache.invalidate(`user:${userId}`);
        await searchIndex.update(userId, profileData);
        return { success: true };
    } catch (error) {
        logger.error('Failed to update user profile', {
            userId,
            error: error.message,
            stack: error.stack
        });
        throw new ProfileUpdateError(
            'Unable to update profile',
            { cause: error }
        );
    }
}

Why this matters: Every error is logged with context, providing debugging information. The error propagates to the caller, allowing proper error handling at the appropriate level. Monitoring systems can alert on these errors, and the application fails fast rather than continuing with invalid state.

Conclusion

Empty catch blocks are never acceptable in production code. Every caught exception needs logging at minimum, and most need to propagate to callers or trigger specific recovery actions. If you genuinely need to ignore an error, document why with a comment explaining the business justification. The default should always be to handle errors explicitly, not silently discard them.

FAQs

Got Questions?

What if I genuinely need to ignore certain errors?

Document it explicitly with a comment explaining why the error is safe to ignore. Log the error at debug level so it appears in verbose logs but doesn't trigger alerts. Consider whether ignoring the error could lead to invalid state. Even for "expected" errors like cache misses or network timeouts, logging helps operations teams understand system behavior patterns.

Should I always log errors in catch blocks?

Logging is usually a good idea because you can't debug issues without seeing what failed. There are cases when you can trace the issue without logs like immediately re-throwing the error to be handled elsewhere, or if the application should crash and restart for critical failures. But proper logging always helps.

What's the difference between logging and re-throwing errors?

Logging records what happened for debugging and monitoring. Re-throwing propagates the error to callers so they can decide how to respond. Do both: log the error with context at the point of failure, then re-throw (possibly wrapped in a more specific error type) to let callers handle recovery. Don't log the same error at multiple levels, it creates noise.

How do I handle errors that occur in finally blocks?

Finally blocks should rarely throw errors. If they must perform error-prone operations (like closing resources), wrap those in their own try-catch. Log any errors but don't let them mask the original error. Some languages provide syntax for handling both the main error and finally-block errors, use these mechanisms to preserve both error contexts.

What about performance impact of logging every error?

Logging is cheap compared to the cost of debugging production issues without logs. Modern logging frameworks are highly optimized. If you have so many errors that logging impacts performance, fix the errors rather than hiding them. High error rates indicate serious problems that empty catch blocks will only make worse.

Should catch blocks always throw errors, or can they return error values?

It depends on the language and architecture. In JavaScript with promises, throwing from catch propagates to the next error handler. Returning an error object from catch resolves the promise with that error, which is usually wrong. Familiarize yourself with your language's error handling semantics. Generally, let errors propagate unless you can meaningfully recover.

How do I handle errors in async operations that don't have try-catch?

Use .catch() handlers on promises, error event listeners on event emitters, or error callbacks in callback-based APIs. Never ignore rejection handlers or error callbacks. Unhandled promise rejections should be monitored at the process level and treated as critical failures. Modern Node.js can terminate on unhandled rejections, which is better than silent failure.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.