Log everything, but log it smartly. Include context such as who made the request, what they were doing, and what went wrong.
// config/logging.php
return [
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Alert',
'level' => 'critical', // Only critical errors to Slack
],
],
];
// Log with rich context
class DomainService
{
public function licenses(Bearer $bearer): Collection
{
Log::info('Fetching domains', [
'bearer_id' => $bearer->id,
'bearer_name' => $bearer->name,
'timestamp' => now()->toIso8601String(),
]);
try {
return LicenseFacade::licenses();
} catch (Exception $e) {
// Log the failure with full context
Log::error('Failed to fetch domains', [
'bearer_id' => $bearer->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}
When something goes wrong, your logs tell the entire story. Not just "error happened" but "which user, when, and why."
Request Logging Middleware
// app/Http/Middleware/LogApiRequests.php
class LogApiRequests
{
public function handle(Request $request, Closure $next): Response
{
$start = microtime(true);
// Don't log health checks (noise)
if ($request->is('*/health')) {
return $next($request);
}
$response = $next($request);
$duration = round((microtime(true) - $start) * 1000); // ms
Log::channel('api')->info('API Request', [
'method' => $request->getMethod(),
'path' => $request->getPathInfo(),
'status' => $response->getStatusCode(),
'duration_ms' => $duration,
'bearer_id' => auth()->id(),
'remote_ip' => $request->ip(),
]);
return $response;
}
}
Error Code System
Create an enum of error codes for consistency. When an error happens, you return a machine-readable code, not just a message:
enum ErrorCode: string
{
// Validation (4xx)
case INVALID_INPUT = 'INVALID_INPUT';
case VALIDATION_FAILED = 'VALIDATION_FAILED';
case MISSING_FIELD = 'MISSING_FIELD';
// Authentication (401)
case INVALID_TOKEN = 'INVALID_TOKEN';
case TOKEN_EXPIRED = 'TOKEN_EXPIRED';
// Authorization (403)
case INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS';
// Not Found (404)
case RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND';
case LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND';
// Rate Limiting (429)
case RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED';
// Server (5xx)
case INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR';
case PROVIDER_ERROR = 'PROVIDER_ERROR';
}
// In exception handler
if ($exception instanceof LicenseNotFound) {
return response()->json([
'error' => 'License not found',
'code' => ErrorCode::LICENSE_NOT_FOUND->value,
], 404);
}
Error codes let API consumers write smarter client code. Instead of checking error messages (fragile!), they check codes.
Summary
Error handling:
- Format all errors consistently
- Never expose stack traces to clients
- Log everything internally
- Use appropriate HTTP status codes
Logging:
- Log with context (who, what, when)
- Use different log channels for different severity
- Log at the boundaries (request/response)
- Keep logs for compliance but clean them up
Custom exceptions:
- Create domain-specific exception classes
- Use enums for error codes
- Handle different exceptions differently
- Make errors help, not hurt
Good error handling turns confusion into clarity. Your future self, debugging at 2 AM, will thank you.