Why must APP_DEBUG be false on a production Laravel app, and what does the debug exception page reveal when it is true?
answer
- config app.debug reads APP_DEBUG
- stack frames with source code
- request headers and body, unmasked
- executed queries with bindings
- JSON gets file, line and trace
basics
~20 sWith APP_DEBUG=true an unhandled error renders Laravel's detailed exception page: stack frames with source code, request headers and body, route details and executed SQL. On a public site that hands attackers secrets and internals, so production keeps it false.
solid answer
~40 s`APP_DEBUG` feeds `config('app.debug')`, which defaults to `false` in `config/app.php` while the skeleton's `.env.example` sets it to `true` for local work. When it is true, an unhandled exception that is not an HTTP exception renders Laravel's exception page: the message and class, stack frames with surrounding source code, the request URL and **all headers** (cookies, `Authorization`), the **request body** as JSON with nothing masked, the route's controller, middleware and parameters, and up to 100 executed queries with bindings filled in. JSON requests get the message, exception class, file, line and a trace. With it false, users get a generic 500 page or `{"message": "Server Error"}`. The docs warn that true in production risks exposing sensitive configuration values; exception messages alone often reveal hosts, users and paths.
code
ini · 7 lines# .env on a developer laptop
APP_ENV=local
APP_DEBUG=true
# .env on the production server
APP_ENV=production
APP_DEBUG=falsego deeper
Know that APP_DEBUG=true shows detailed error pages, that it belongs only on local machines, and that production must set it to false.
List what the exception page and the debug JSON body contain, and explain how config('app.debug') is read and cached.
Explain the concrete leaks (headers, body, queries, messages) and how to diagnose production errors from logs without enabling debug mode.
Build safeguards so debug mode cannot reach production by accident, such as deploy checks and environment-specific configuration review.
## Where the switch lives `config/app.php` contains `'debug' => (bool) env('APP_DEBUG', false)`. The default is `false`; the skeleton's `.env.example` sets `APP_DEBUG=true` because a new project starts as a local development copy. Everything in the framework that asks "am I in debug mode?" reads `config('app.debug')` or `$app->hasDebugModeEnabled()`. ## What debug mode changes When an exception escapes your code, Laravel's exception handler decides what to send back. With debug mode on: - **HTML requests** get Laravel's own exception page for exceptions that are not HTTP exceptions (an `abort(404)` still renders its normal error page). - **JSON requests** get a body with `message`, `exception` (the class), `file`, `line` and `trace` (frames without their arguments). - The framework registers a listener that records executed **queries** so the page can list them. With debug mode off, the same exception produces a plain 500 page, and JSON clients receive `{"message": "Server Error"}`. HTTP exceptions keep their own message in both modes. ## What the exception page shows The page is built by `Illuminate\Foundation\Exceptions\Renderer` and shows: | Section | Content | Why it is dangerous in public | |---|---|---| | Header | exception class and message | messages often embed hosts, usernames, file paths, SQL | | Trace | every frame, with source code around each line | reveals application structure and code | | Request | URL, method and **all headers** | `Cookie` and `Authorization` headers expose sessions and tokens | | Body | `$request->all()` as JSON, unmasked | passwords and card fields from the failing form | | Routing | controller, route name, middleware, parameters | bound models appear with their attributes | | Queries | up to 100 queries with bindings substituted | table layout and real data values | The docs put it bluntly: with `APP_DEBUG` true in production "you risk exposing sensitive configuration values to your application's end users". A database connection failure is a typical example: its message names the host and the user it tried. ## Other things tied to debug mode - **Debugbar**, if installed, only enables itself when debug mode is on and the environment is neither `production` nor `testing`. - Many packages show extra diagnostics only in debug mode, so turning it on "just for a minute" in production enables more than the exception page. ## The grocery app locally On the local copy of the grocery app, debug mode is what you want: when the discount calculation throws, the page shows the failing line in `DiscountService`, the cart id in the route parameters, and the promotion queries that ran just before. That is the fastest route to the bug. The same page on the live store would show every visitor the coupon codes in the request body and the session cookie of whoever triggered it. ## What does not change Debug mode only changes what is **shown**. Reporting is the same either way: the handler still logs the exception with its trace to the configured channels, so production logs keep the detail the page would have displayed. ## Getting detail in production safely 1. Keep `APP_DEBUG=false` and read the **logs**, where the handler reports the full exception and trace. 2. Reproduce locally with debug on, using the log's request details. 3. If you must see more live, add targeted `Log::debug()` lines or context rather than flipping the switch. 4. Apply the same rule to staging and preview servers that testers or clients can reach: anyone who sees the page sees the data. ## Common misunderstandings - Debug mode does not only affect HTML pages; JSON APIs leak file paths and traces too. - Setting `APP_ENV=production` does not turn debug off; the two variables are independent. - Hiding the page behind a custom 500 view does not help if debug mode is still on, because the handler renders the debug page before falling back to views.
- With APP_DEBUG=false, how do you still find out what caused a production 500 error?The exception handler still reports the exception to the log channels with its full message and stack trace, so check `storage/logs` or your log service. Add request identifiers or context to correlate it with the failing request, reproduce locally with debug mode on, and add targeted `Log::debug()` lines if the log is not enough.
- What does a JSON API client receive for an unhandled exception when APP_DEBUG is true?A JSON body with `message`, `exception` (the class name), `file`, `line` and `trace`, an array of stack frames with their arguments removed. With debug off the same failure returns only `{"message": "Server Error"}` for non-HTTP exceptions, which is why an API must also run with debug disabled.
saying these in an interview costs you the question
- APP_DEBUG only affects HTML pages, so APIs can keep it on.
- Setting APP_ENV=production automatically disables debug mode.
- The debug page masks password fields in the request body.
- A custom errors/500.blade.php view hides the debug page even with APP_DEBUG=true.
- config/app.php defaults debug to true when APP_DEBUG is missing.