Silent Failure Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
silent-failure | 🏅 Best Practices | High | 20 minutes |
Detects empty catch blocks and error suppression that hide failures, making debugging impossible and masking critical production issues.
What This Checks
The Silent Failure Analyzer identifies four dangerous error handling patterns that hide exceptions and errors, making bugs impossible to debug in production:
Detected Patterns:
Empty Catch Blocks (High Severity)
catch (Exception $e) { }- Completely swallows exceptions- No logging, no rethrow, no error handling
Catch Without Logging or Rethrow (Medium Severity)
catch (Exception $e) { return null; }- Handles exception but doesn't log- Missing
Log::error(),report(), orthrow - Silently fails without any trace
Broad Exception Catch Without Logging (High/Medium Severity)
catch (\Throwable $e) { ... }— catchingThrowable,Exception, orErroris overly broad and can mask fatal programming errors likeTypeErrororArgumentCountError- High — no logging at all (genuinely silent failure)
- Medium — logging present but exception variable unused (error details are lost)
- Not flagged when logging is present and the exception variable is used (e.g.
Log::error('...', ['error' => $e->getMessage()])) — this is considered well-handled
Error Suppression Operator (High/Medium Severity)
@file_get_contents($path)- Hides all errors and warnings@mysql_query($sql)- Suppresses error messages- Makes debugging impossible when things go wrong
Why It Matters
Silent failures are one of the most dangerous anti-patterns in production applications:
1. Impossible Debugging
When exceptions are swallowed silently, you have no way to know what went wrong:
Before (❌):
class PaymentProcessor
{
public function charge($amount)
{
try {
$result = $this->gateway->charge($amount);
return $result;
} catch (Exception $e) {
// Silent failure - payment fails but no one knows
return false;
}
}
}
// Production: Customers can't checkout, no logs, no alerts, no idea whyAfter (✅):
class PaymentProcessor
{
public function charge($amount)
{
try {
$result = $this->gateway->charge($amount);
return $result;
} catch (Exception $e) {
// Log the exception for debugging
Log::error('Payment processing failed', [
'amount' => $amount,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Report to error tracking service
report($e);
// Still return false, but now we know what happened
return false;
}
}
}2. Hidden Security Vulnerabilities
Silent failures can mask security issues:
Before (❌):
class AuthService
{
public function validateToken($token)
{
try {
return JWT::decode($token, $this->key);
} catch (Exception $e) {
// Token validation fails silently
// Attacker has no idea their attack failed
return null;
}
}
}After (✅):
class AuthService
{
public function validateToken($token)
{
try {
return JWT::decode($token, $this->key);
} catch (Exception $e) {
// Log potential security issue
Log::warning('Invalid JWT token attempted', [
'token' => substr($token, 0, 20) . '...',
'ip' => request()->ip(),
]);
return null;
}
}
}3. Data Loss and Corruption
Without error reporting, data operations fail silently:
Before (❌):
class DataImporter
{
public function import($file)
{
try {
$data = $this->parse($file);
$this->save($data);
} catch (Exception $e) {
// Import fails, data is lost, no one knows
}
}
}After (✅):
class DataImporter
{
public function import($file)
{
try {
$data = $this->parse($file);
$this->save($data);
} catch (Exception $e) {
Log::error('Data import failed', [
'file' => $file,
'error' => $e->getMessage(),
]);
// Optionally rethrow to bubble up
throw new ImportException('Failed to import data', 0, $e);
}
}
}4. Broad Catches That Mask Programming Errors
Catching \Throwable swallows fatal errors like TypeError, ArgumentCountError, and Error that indicate bugs in your own code:
Before (❌):
class CheckRunner
{
public function run()
{
try {
$this->executeChecks();
} catch (\Throwable $e) {
// Masks TypeError, ArgumentCountError, etc.
$this->check->markAsFailed('Unknown error');
}
}
}After (✅):
class CheckRunner
{
public function run()
{
try {
$this->executeChecks();
} catch (\Throwable $e) {
// Log first — ensures observability even if markAsFailed throws
Log::error('Check run failed', [
'check_id' => $this->check->id,
'error' => $e->getMessage(),
'class' => get_class($e),
]);
$this->check->markAsFailed($e->getMessage());
}
}
}This pattern is common in Jobs and API controllers where top-level handlers must catch \Throwable to update external state. The analyzer does not flag it as long as Log::error() (or equivalent) is called and the exception variable is used.
How to Fix
Quick Fix (~5 minutes per catch block)
For simple cases, add logging to catch blocks:
Before (❌):
class OrderService
{
public function createOrder($data)
{
try {
return Order::create($data);
} catch (Exception $e) {
return null;
}
}
}After (✅):
class OrderService
{
public function createOrder($data)
{
try {
return Order::create($data);
} catch (Exception $e) {
Log::error('Order creation failed', [
'data' => $data,
'error' => $e->getMessage(),
]);
return null;
}
}
}Steps:
- Add
use Illuminate\Support\Facades\Log;at the top - Call
Log::error()in catch block with context - Include relevant data for debugging
- Consider using
report($e)for error tracking services
Proper Fix (~20 minutes per class)
For production code, implement comprehensive error handling:
Before (❌):
class EmailService
{
public function send($user, $message)
{
try {
Mail::to($user)->send($message);
} catch (Exception $e) {
// Silent failure
}
}
public function readTemplate($path)
{
return @file_get_contents($path);
}
}After (✅):
class EmailService
{
public function send($user, $message)
{
try {
Mail::to($user)->send($message);
} catch (Exception $e) {
// Log with full context
Log::error('Email sending failed', [
'user_id' => $user->id,
'email' => $user->email,
'message_class' => get_class($message),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Report to error tracking (Sentry, Bugsnag, etc.)
report($e);
// Optionally rethrow for critical failures
if ($message instanceof CriticalNotification) {
throw $e;
}
}
}
public function readTemplate($path)
{
// Replace error suppression with proper error handling
if (!file_exists($path)) {
Log::warning('Email template not found', ['path' => $path]);
return $this->getDefaultTemplate();
}
$content = file_get_contents($path);
if ($content === false) {
Log::error('Failed to read email template', ['path' => $path]);
throw new TemplateException("Cannot read template: {$path}");
}
return $content;
}
}Steps:
- Add comprehensive logging with context
- Integrate error tracking (Sentry, Bugsnag, Rollbar)
- Replace
@operator with proper error handling - Add graceful fallbacks where appropriate
- Rethrow critical exceptions
- Document expected exceptions
ShieldCI Configuration
To configure whitelists, publish the config:
php artisan vendor:publish --tag=shieldci-configThen in config/shieldci.php:
'analyzers' => [
'best-practices' => [
'enabled' => true,
'silent-failure' => [
// Directories to skip (tests can have empty catches)
'whitelist_dirs' => [
'tests',
'database/seeders',
'database/factories',
],
// Class patterns to skip
'whitelist_classes' => [
'*Test', // PHPUnit tests
'*TestCase', // Test base classes
'*Seeder', // Database seeders
'DatabaseSeeder', // Main seeder
],
// Exception types that are expected and can be safely caught
'whitelist_exceptions' => [
'ModelNotFoundException', // Expected when using findOrFail
'NotFoundException', // Custom not found exceptions
'NotFoundHttpException', // HTTP 404 exceptions
'ValidationException', // Validation failures
],
// Functions where @ operator is acceptable (supports namespaced: @\unlink())
'whitelist_error_suppression_functions' => [
'unlink', // File might not exist
'fopen', // File might not be readable
'file_get_contents', // File might not exist
'mkdir', // Directory might exist
'rmdir', // Directory might not exist
],
// Static methods where @ operator is acceptable (e.g., @Storage::delete())
// Wildcards supported: 'Storage::*', '*::delete'
'whitelist_error_suppression_static_methods' => [
'Storage::delete', // File might not exist
'Storage::deleteDirectory', // Directory might not exist
'File::delete', // File might not exist
'File::deleteDirectory', // Directory might not exist
],
// Instance methods where @ operator is acceptable (e.g., @$file->delete())
// Matches by method name only. Wildcards supported: 'remove*'
'whitelist_error_suppression_instance_methods' => [
'delete', // Resource deletion
'close', // File handle closing
'unlink', // File unlinking
],
],
],
],Error Suppression Whitelist Details
The error suppression whitelist supports three types of calls:
| Call Type | Example | Config Key |
|---|---|---|
| Function calls | @unlink($path), @\unlink($path) | whitelist_error_suppression_functions |
| Static method calls | @Storage::delete($path) | whitelist_error_suppression_static_methods |
| Instance method calls | @$file->delete() | whitelist_error_suppression_instance_methods |
Wildcard patterns are supported in all whitelist keys:
Storage::*- matches any method on Storage*::delete- matches delete on any classremove*- matches removeFile, removeAll, etc.
Dynamic calls are always flagged for security reasons:
@$func($path)- dynamic function call@$class::method()- dynamic class@$obj->$method()- dynamic method name
References
- Laravel Error Handling - Official Laravel documentation
- PSR-3 Logger Interface - Standard logging interface
- Sentry for Laravel - Error tracking integration
- Bugsnag Laravel Integration - Alternative error tracking
- Logging Best Practices - Comprehensive logging guide
- Exception Handling Patterns - Martin Fowler on exceptions
Related Analyzers
- Service Container Resolution - Avoid manual container resolution that hides dependencies
- Debug Mode Analyzer - Detects debug mode enabled in production