skip to content

In PHP 8.4 and 8.5, what happens when code calls a function marked #[\Deprecated] or ignores the result of one marked #[\NoDiscard]?

level: middleimportance: nice to knowfreq 15%

answer

  1. engine-consumed, not just metadata
  2. Deprecated(message, since): E_USER_DEPRECATED per call
  3. NoDiscard: warning when result unused
  4. (void) cast suppresses NoDiscard
  5. DateTimeImmutable::modify() is NoDiscard in 8.5

basics

~20 s

Calling a #[\Deprecated] user function emits E_USER_DEPRECATED, including the optional since and message. Calling a #[\NoDiscard] function (PHP 8.5) as a bare statement emits a warning unless the result is used or cast with (void).

solid answer

~50 s

`#[\Deprecated]` (PHP 8.4) goes on functions, methods and class constants; 8.5 added global constants and traits. Each use raises `E_USER_DEPRECATED`, for example `Function old() is deprecated since 1.5, use new() instead`, built from the optional `since` and `message` arguments; `ReflectionFunctionAbstract::isDeprecated()` reports it. It cannot mark properties or ordinary classes: `#[\Deprecated]` on a non-trait class is a compile error. `#[\NoDiscard]` (PHP 8.5) goes on functions and methods; if a call's return value is not used, PHP emits `E_USER_WARNING` for userland functions (`E_WARNING` for internal ones): `The return value of function f() should either be used or intentionally ignored by casting it as (void)`. `(void) f();` suppresses it. It is a compile error on `void` or `never` functions. PHP 8.5 applies it to `DateTimeImmutable::modify()`, `add()`, `sub()`, `setTimezone()` and `setTime()`, catching code that expects them to mutate the object.

code

php · 18 lines
php
<?php
declare(strict_types=1);

#[\Deprecated(message: 'use sendV2() instead', since: '3.0')]
function send(string $to): bool { return true; }

#[\NoDiscard('the result lists failed recipients')]
function sendV2(array $to): array { return []; }

send('[email protected]');
// Deprecated: Function send() is deprecated since 3.0, use sendV2() instead

sendV2(['[email protected]']);
// Warning: The return value of function sendV2() should either be used or
// intentionally ignored by casting it as (void), the result lists failed recipients

$failed = sendV2(['[email protected]']);   // used: no warning
(void) sendV2(['[email protected]']);      // intentionally ignored: no warning

go deeper

for a junior

Know that PHP can warn callers: #[\Deprecated] flags an old function, #[\NoDiscard] flags an ignored result, and (void) marks intentional discarding.

for a middle

Explain the valid targets, the message format built from message and since, the E_USER_DEPRECATED and E_USER_WARNING levels, and the void/never compile error.

for a senior

Use these attributes to evolve a library API safely, understand why interface-level NoDiscard is ineffective, and spot the DateTimeImmutable::modify() bug they now reveal.

for a principal

Plan deprecation policy across releases: when to mark, what the since value means to consumers, and how long deprecated paths stay before removal.

## Attributes the engine acts on Most PHP attributes are inert metadata that a library reads. A few are **consumed by the engine itself**. Two of them exist to guide callers of an API: `#[\Deprecated]` (PHP 8.4) and `#[\NoDiscard]` (PHP 8.5). Both produce diagnostics at run time, at the call, rather than changing what the function does. ## #[\Deprecated] The class is `Deprecated`, with two optional readonly properties set from the constructor: `?string $message` and `?string $since`. **Where it may go:** - functions and methods (8.4); - class constants (8.4); - global `const` declarations (8.5); - traits (8.5) — the deprecation fires when a class `use`s the trait. **Where it may not:** properties and parameters are not valid targets, and on a class that is not a trait it fails at compile time with `Cannot apply #[\Deprecated] to class ...`. **What happens:** each call or constant fetch emits `E_USER_DEPRECATED` for userland code (engine-internal deprecations use `E_DEPRECATED`). The text is assembled from the arguments: ``` Deprecated: Function unsafe_function() is deprecated since 1.5, use safe_replacement() instead ``` The function still runs normally afterwards. Reflection sees the flag: `ReflectionFunctionAbstract::isDeprecated()` and `ReflectionClassConstant::isDeprecated()` return `true`. Before 8.4, library authors called `trigger_error(..., E_USER_DEPRECATED)` by hand inside the function; the attribute moves that into the declaration and makes it visible to tooling. ## #[\NoDiscard] The class is `NoDiscard`, with an optional `?string $message`. It targets functions and methods only. **What happens:** when a call's result is not used — the call is a bare expression statement — PHP emits a warning: ``` The return value of function bulk_process() should either be used or intentionally ignored by casting it as (void), as processing might fail for individual items ``` The level is `E_USER_WARNING` for userland functions and `E_WARNING` for internal ones. The result counts as used when it is assigned (even to `$_`), passed on, returned or otherwise consumed. **Opting out:** PHP 8.5 also added the `(void)` cast. `(void) some_command();` states that discarding is intentional and silences the warning. It is a statement form: it has no value and cannot be used inside an expression. **Compile-time rules:** 1. a `void` or `never` function cannot carry it — it has no value to use; 2. property hooks cannot carry it; 3. it applies to the declaration actually called: marking an interface or abstract method does nothing for implementations that do not repeat it, while a method imported from a trait keeps it. ## A built-in example worth knowing PHP 8.5 marks several `DateTimeImmutable` methods with `#[\NoDiscard]`: `modify()`, `add()`, `sub()`, `setTimezone()` and `setTime()`. The classic bug — ```php $date = new DateTimeImmutable('2026-01-01'); $date->modify('+1 day'); // warning in 8.5: the object is not changed ``` — now produces a warning explaining that the method does not modify the object itself. ## Reading them through reflection and tooling Because both are real attributes, tools can inspect them without calling anything: - `ReflectionFunctionAbstract::isDeprecated()` and `ReflectionClassConstant::isDeprecated()` report the flag directly; - `getAttributes(Deprecated::class)[0]->newInstance()` exposes the `message` and `since` values, which documentation generators and upgrade tools display; - `getAttributes(NoDiscard::class)` works the same way for the `message` of `#[\NoDiscard]`. Static analysers and IDEs read the same declarations, so a deprecated call can be flagged in review before it ever runs, while the engine's diagnostic catches the calls that analysis cannot see, such as dynamic calls through variable function names. ## Side by side | | `#[\Deprecated]` | `#[\NoDiscard]` | |---|---|---| | Added | 8.4 (constants and traits 8.5) | 8.5 | | Arguments | `message`, `since` | `message` | | Fires when | the function, constant or trait is used | the return value is not used | | Userland level | `E_USER_DEPRECATED` | `E_USER_WARNING` | | Opt-out at call site | none; stop calling it | `(void)` cast or use the value | Whether either diagnostic is displayed or logged depends on the error-reporting configuration, which is a separate concern. On PHP 8.3 and earlier both attributes are ignored as unknown global classes, so adding them to a library is backward-compatible.

  • Can a PHP library add #[\NoDiscard] while still supporting PHP 8.4?
    Yes. On 8.4 the attribute is just an unknown class name that nothing instantiates, so it has no effect. Callers there cannot use `(void)`, which is 8.5 syntax; assigning to a throwaway variable such as `$_` works on both versions.
  • Why does marking an interface method #[\NoDiscard] not warn callers in PHP 8.5?
    The check uses the declaration that is actually invoked. A call through the interface runs the implementing class's method, which has no attribute unless it repeats it. Trait methods differ: they are copied into the using class and keep the attribute.

saying these in an interview costs you the question

  • #[\Deprecated] stops the deprecated function from running.
  • #[\Deprecated] can mark a property or an ordinary class in PHP 8.5.
  • #[\NoDiscard] turns an ignored return value into a thrown exception.
  • (void) $x = f(); is needed because assignment alone still warns.
  • DateTimeImmutable::modify() changes the object, so its result can be ignored.