DTO usage in Laravel — clean data flow from request to service
Concept
Error handling and logging in Laravel are configured via bootstrap/app.php (Laravel 11+) or app/Exceptions/Handler.php (Laravel 10 and below). Laravel wraps exceptions and returns appropriate HTTP responses. Logging uses the PSR-3 Log facade backed by Monolog.
Exception rendering: The exception handler converts exceptions to HTTP responses. ModelNotFoundException → 404. AuthorizationException → 403. ValidationException → 422. Custom exceptions can define render($request) and report() methods.
report(callable): The report() method on an exception determines how it's logged. By default, exceptions are logged to the configured log channel. Override report() to send to Sentry, Slack, etc.
renderable(callable) in bootstrap/app.php:
->withExceptions(function(Exceptions $exceptions) {
$exceptions->renderable(fn(MyException $e, $request) => response()->json(['error' => $e->getMessage()], 422));
})Custom exception classes: Extend Exception. Add render() method for HTTP response. Add report() for logging behavior. Add context() to include extra data in logs.
Log facade channels: Log::info(), Log::warning(), Log::error(), Log::critical(). Structured logging: Log::info('User logged in', ['user_id' => $user->id]). Channels: Log::channel('slack')->critical('Payment failed').
withContext(array $context): Add context that's included in every subsequent log call: Log::withContext(['user_id' => auth()->id()]).
Code Example
<?php
// Custom exception with render() and report()
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class OrderException extends Exception
{
public function __construct(
string $message,
private readonly int $orderId,
private readonly string $reason,
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
// Rendered to HTTP response
public function render(Request $request): JsonResponse
{
return response()->json([
'error' => $this->getMessage(),
'reason' => $this->reason,
], 422);
}
// Custom report behavior
public function report(): void
{
\Illuminate\Support\Facades\Log::error('Order failed', [
'order_id' => $this->orderId,
'reason' => $this->reason,
'message' => $this->getMessage(),
]);
// Also send to Sentry if configured:
// \Sentry\captureException($this);
}
}
// bootstrap/app.php (Laravel 11+) — global exception handling
->withExceptions(function(\Illuminate\Foundation\Configuration\Exceptions $exceptions) {
// Map exception to response
$exceptions->renderable(function(\App\Exceptions\PaymentException $e, Request $request) {
return response()->json(['error' => 'Payment processing failed', 'code' => $e->getCode()], 402);
});
// Log additional context
$exceptions->context(fn() => ['user_id' => auth()->id()]);
// Don't report specific exceptions
$exceptions->dontReport(\App\Exceptions\RateLimitExceededException::class);
})
// Logging
\Illuminate\Support\Facades\Log::info('Post created', ['post_id' => $post->id, 'user_id' => $user->id]);
\Illuminate\Support\Facades\Log::channel('slack')->critical('Payment gateway down');
\Illuminate\Support\Facades\Log::withContext(['request_id' => $requestId]); // per-request context