Laravel Tutorial · Requests and Responses · Lesson 12 of 27

Errors, Exceptions and Logging

Stop requests with abort(), throw custom exceptions that render their own responses, report errors to logs, and know what debug mode changes.

Create a free account to save this page, take notes and track your progress. Sign in

abort and HTTP exceptions

abort(403), abort(404, 'Not found'), abort_if($cond, 403) and abort_unless(...) throw an HTTP exception that Laravel converts into a response: a JSON error for API requests, an error page for browsers (resources/views/errors/404.blade.php customises it).

Custom exceptions

An exception class can define render() (what the user gets) and report() (what's logged). Or configure rendering centrally in bootstrap/app.php with ->withExceptions(fn ($exceptions) => $exceptions->render(...)).

abort(), a custom renderable exception, and hidden internals
<?php
use Illuminate\Support\Facades\Route;

class InsufficientStock extends RuntimeException
{
    public function __construct(public string $sku, public int $available)
    {
        parent::__construct("Not enough stock for $sku");
    }

    public function render()
    {
        return response()->json([
            'error' => 'insufficient_stock',
            'message' => "Only {$this->available} left of {$this->sku}.",
        ], 409);
    }
}

$stock = ['MUG' => 2];

Route::post('/cart/{sku}', function (string $sku) use ($stock) {
    abort_unless(isset($stock[$sku]), 404, "Unknown product $sku");
    $qty = (int) request('qty', 1);
    if ($qty > $stock[$sku]) {
        throw new InsufficientStock($sku, $stock[$sku]);
    }
    return ['added' => $qty];
});
Route::get('/admin', fn () => abort(403));
Route::get('/broken', fn () => throw new LogicException('database password is hunter2'));

visit('POST', '/cart/MUG', ['qty' => 1]);
visit('POST', '/cart/MUG', ['qty' => 5]);
visit('POST', '/cart/TEA');
visit('GET', '/admin');
visit('GET', '/broken');      // debug mode is off: the message is hidden from the client

Output

POST /cart/MUG -> 200 {"added":1}
POST /cart/MUG -> 409 {"error":"insufficient_stock","message":"Only 2 left of MUG."}
POST /cart/TEA -> 404 {
    "message": "Unknown product TEA"
}
GET /admin -> 403 {
    "message": ""
}
GET /broken -> 500 {
    "message": "Server Error"
}

Run this on your computer — see “Get Started” for setup.

Debug mode and logging

  • APP_DEBUG=true shows detailed error pages with stack traces: never in production, it leaks code and secrets.
  • report($e) logs an exception without stopping the request; Log::error('message', ['order' => $id]) writes structured log entries to the channel configured in config/logging.php (files, stderr, Slack, external services).

Test yourself

Why must APP_DEBUG be false in production?

  1. It makes the app slower
  2. Debug error pages expose stack traces, code and configuration details
  3. It disables logging
  4. Routes stop working
Show answer

Answer: Debug error pages expose stack traces, code and configuration details

Was this page helpful?