In Laravel 13, how do you log PHP and framework deprecation warnings, and why do none appear in a freshly installed app?
answer
- LOG_DEPRECATIONS_CHANNEL=null by default
- or define a channel named deprecations
- written at warning level
- LOG_DEPRECATIONS_TRACE adds a stack trace
- skipped in tests without LOG_DEPRECATIONS_WHILE_TESTING
basics
~20 sLaravel sends E_DEPRECATED and E_USER_DEPRECATED notices to its deprecations channel, which the skeleton points at the null channel. Set LOG_DEPRECATIONS_CHANNEL to a real channel, or define a channel named deprecations, and they are logged as warnings.
solid answer
~40 sLaravel's error handler treats PHP `E_DEPRECATED` and `E_USER_DEPRECATED` notices specially: instead of throwing an `ErrorException`, it logs them to a `deprecations` channel at the **warning** level, as '<message> in <file> on line <n>'. The skeleton's `config/logging.php` sets `'deprecations' => ['channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false)]`, and `.env.example` sets `LOG_DEPRECATIONS_CHANNEL=null`, so by default they go to the `null` channel and vanish. Point the variable at a channel such as `daily`, or define a channel literally named `deprecations`, which always wins. `LOG_DEPRECATIONS_TRACE=true` attaches an exception so the stack trace is logged. During tests deprecations are ignored unless `LOG_DEPRECATIONS_WHILE_TESTING` is set. The target channel's level must be `warning` or lower, or every entry is dropped.
code
php · 13 lines<?php
// config/logging.php (excerpt): a dedicated file that always wins
return [
'channels' => [
'deprecations' => [
'driver' => 'daily',
'path' => storage_path('logs/deprecations.log'),
'level' => 'warning',
'max_files' => 7,
],
],
];go deeper
Know that deprecation warnings are discarded by default and LOG_DEPRECATIONS_CHANNEL turns them on.
Explain the warning level, the deprecations channel taking priority, the trace option and the testing switch.
Use deprecation logs to plan PHP and framework upgrades without flooding production logs or alert channels.
Build deprecation tracking into the upgrade cadence so warnings are cleared before they become breaking changes.
## What counts as a deprecation A **deprecation** is a notice that a feature still works but will be removed or changed: PHP raises `E_DEPRECATED` for language features on their way out, and libraries, including Laravel and its dependencies, raise `E_USER_DEPRECATED` through `trigger_error()`. They are the early warning for the next PHP or framework upgrade. ## How Laravel handles them During bootstrap Laravel installs its own PHP error handler. For most PHP errors it throws an `ErrorException`, so a warning in your code becomes a real exception. Deprecations are the exception to that rule: 1. The handler sees `E_DEPRECATED` or `E_USER_DEPRECATED`. 2. It does not throw. It resolves the log manager and makes sure a `deprecations` channel exists. 3. It writes a **warning**-level entry to that channel: `<message> in <file> on line <n>`, or, with tracing on, the message plus an `ErrorException` so the formatter prints a stack trace. 4. Any failure while logging is swallowed, so a deprecation can never break a request. ## Why Laravel logs rather than throws Turning deprecations into exceptions would make every upgrade of PHP or of a dependency a potential outage: a library raising a new `E_USER_DEPRECATED` notice from inside its own code would crash requests that did nothing wrong. Logging keeps behaviour unchanged while still recording the signal. The trade-off is that the signal is easy to ignore, which is why deprecations need a deliberate destination and a periodic review rather than being left on the `null` channel forever. ## Why nothing appears by default The skeleton's `config/logging.php` contains: ```php 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], ``` and `.env.example` sets `LOG_DEPRECATIONS_CHANNEL=null`. The `null` channel uses Monolog's `NullHandler`, which discards everything. Deprecations are processed and then thrown away, which keeps production logs quiet. ## Turning them on There are two ways, and one takes priority: - **A channel literally named `deprecations`** under `channels`. If it exists, Laravel always uses it, whatever `logging.deprecations` says. It is the tidy option when deprecations need their own file. - **`LOG_DEPRECATIONS_CHANNEL=daily`** (or any configured channel). Laravel copies that channel's config into a `deprecations` channel at runtime, so entries go wherever the named channel writes. Options worth knowing: | Setting | Effect | |---|---| | `LOG_DEPRECATIONS_TRACE=true` | Attaches an `ErrorException`, so entries carry a stack trace showing the caller | | `LOG_DEPRECATIONS_WHILE_TESTING` set | Logs deprecations during the test suite, which are otherwise skipped | | Target channel `level` | Must be `warning` or lower; a `critical`-only channel drops every deprecation | ## Using them before an upgrade A practical routine before moving to a new PHP or Laravel release: - send deprecations to their own file in staging, with tracing on, so each entry names the calling code; - run the test suite with `LOG_DEPRECATIONS_WHILE_TESTING` set, since tests exercise paths production rarely hits; - group the entries by message and fix your own code first; notices raised inside vendor packages usually mean upgrading that package; - turn tracing off again afterwards, because traces make each entry large. ## What an entry looks like With `LOG_DEPRECATIONS_CHANNEL=daily` and tracing off, a PHP 8.4+ deprecation raised when a class is compiled appears as a single warning line: ``` [2026-09-29 09:02:11] staging.WARNING: App\Services\FareQuote::price(): Implicitly marking parameter $promo as nullable is deprecated, the explicit nullable type must be used instead in /var/www/app/Services/FareQuote.php on line 42 ``` The message comes from PHP; Laravel appends the file and line. Many deprecations repeat each time their code path runs, so a busy app logs the same message many times: group by message when reviewing, rather than reading line by line. With `LOG_DEPRECATIONS_TRACE=true` the line keeps only the message, and the formatter prints the attached exception's stack trace underneath, which is what you need when the deprecated call sits inside a vendor package and you must find which of your calls reached it. ## Pitfalls - Pointing `LOG_DEPRECATIONS_CHANNEL` at an alert channel such as Slack at `critical`: warnings never pass its level, so nothing arrives, and at a lower level the chat floods. - Pointing it at the default stack, which mixes thousands of repeated notices into the main application log. - Expecting deprecations to fail tests; Laravel never turns them into exceptions.
- Why do deprecations not show up while the test suite runs?Laravel skips deprecation logging when the app is running unit tests, unless the `LOG_DEPRECATIONS_WHILE_TESTING` environment variable is set. Set it in the test environment to collect them during a run.
- Does Laravel turn a deprecation into an exception like other PHP warnings?No. Other PHP errors reported by `error_reporting()` become `ErrorException`s, but `E_DEPRECATED` and `E_USER_DEPRECATED` are only logged at warning level to the deprecations channel, and any failure while logging them is ignored.
saying these in an interview costs you the question
- Laravel turns deprecation notices into ErrorException like other warnings
- Deprecations are logged at the error level
- LOG_DEPRECATIONS_CHANNEL overrides a channel named deprecations
- Deprecations are logged during tests by default
- Pointing deprecations at a critical-only Slack channel posts them all