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 three 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
Error Suppression Operator (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);
}
}
}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
Configuration
To reduce false positives, configure whitelists 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
'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
],
],
],
],Common Mistakes to Avoid
1. Logging Without Context
Before (❌):
catch (Exception $e) {
Log::error('Error'); // What error? Where? Why?
}After (✅):
catch (Exception $e) {
Log::error('Payment processing failed', [
'order_id' => $order->id,
'amount' => $amount,
'gateway' => $gateway,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}2. Catching Too Broadly
Before (❌):
try {
$user = User::findOrFail($id);
$this->sendEmail($user);
$this->logActivity($user);
} catch (Exception $e) {
// Which operation failed? User lookup, email, or logging?
Log::error('Something failed');
}After (✅):
try {
$user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
Log::warning('User not found', ['id' => $id]);
return null;
}
try {
$this->sendEmail($user);
} catch (Exception $e) {
Log::error('Email sending failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
try {
$this->logActivity($user);
} catch (Exception $e) {
// Activity logging is non-critical, just log the error
Log::warning('Activity logging failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}3. Using @ Instead of Proper Error Handling
Before (❌):
$config = @json_decode($json, true);
if ($config === null) {
// Was it invalid JSON or valid null? Who knows!
return [];
}After (✅):
try {
$config = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
Log::error('Invalid JSON configuration', [
'json' => substr($json, 0, 100),
'error' => $e->getMessage(),
]);
return [];
}4. Not Using Error Tracking Services
Before (❌):
catch (Exception $e) {
Log::error($e->getMessage());
// Logs buried in files, no alerts, no aggregation
}After (✅):
catch (Exception $e) {
// Use Laravel's report() which integrates with Sentry, Bugsnag, etc.
report($e);
// Or use service directly
\Sentry\captureException($e);
// Still log locally
Log::error('Payment failed', [
'exception' => $e,
'context' => $additionalContext,
]);
}5. Graceful Fallback Without Logging
Before (❌):
try {
return $this->fetchUserAvatar($userId);
} catch (NotFoundException $e) {
// Graceful fallback is good, but we should still log
return $this->getDefaultAvatar();
}After (✅):
try {
return $this->fetchUserAvatar($userId);
} catch (NotFoundException $e) {
// Log even for expected exceptions
Log::info('User avatar not found, using default', [
'user_id' => $userId,
]);
return $this->getDefaultAvatar();
}6. Empty Catch for Performance
Before (❌):
// "I don't care about errors, just skip invalid items"
foreach ($items as $item) {
try {
$this->process($item);
} catch (Exception $e) {
// Silent skip
}
}After (✅):
$skipped = 0;
foreach ($items as $item) {
try {
$this->process($item);
} catch (Exception $e) {
$skipped++;
// Log first few errors, then summarize
if ($skipped <= 5) {
Log::warning('Item processing failed', [
'item' => $item,
'error' => $e->getMessage(),
]);
}
}
}
if ($skipped > 0) {
Log::warning("Skipped {$skipped} invalid items during batch processing");
}Error Reporting Best Practices
1. Use Laravel's report() Helper
Laravel's report() function integrates with error tracking services:
catch (Exception $e) {
// Automatically sends to configured service (Sentry, Bugsnag, etc.)
report($e);
}2. Integrate Error Tracking Services
Sentry:
catch (Exception $e) {
\Sentry\captureException($e);
// Add context
\Sentry\configureScope(function ($scope) use ($user, $order) {
$scope->setUser(['id' => $user->id, 'email' => $user->email]);
$scope->setExtra('order_id', $order->id);
});
}Bugsnag:
catch (Exception $e) {
Bugsnag::notifyException($e, function ($report) use ($context) {
$report->setMetaData(['context' => $context]);
});
}3. Use Appropriate Log Levels
// CRITICAL: Application is unusable
Log::critical('Database connection lost');
// ERROR: Runtime errors that need immediate attention
Log::error('Payment processing failed', ['order_id' => $order->id]);
// WARNING: Exceptional but handled, should be investigated
Log::warning('Cache server unavailable, using fallback');
// INFO: Interesting events
Log::info('User avatar not found, using default');
// DEBUG: Detailed debug information (development only)
Log::debug('API response', ['response' => $data]);4. Structure Log Messages
Log::error('Operation failed', [
'operation' => 'payment_processing',
'user_id' => $user->id,
'amount' => $amount,
'currency' => 'USD',
'gateway' => $gateway,
'error' => $e->getMessage(),
'error_code' => $e->getCode(),
'trace' => $e->getTraceAsString(),
'timestamp' => now()->toIso8601String(),
]);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