6 min read

Stop Ignoring Important Return Values with PHP 8.5's #[NoDiscard]

PHP 8.5's #[NoDiscard] attribute warns you when critical return values are silently discarded. Here's how to use it to write safer, self-documenting APIs.

Featured image for "Stop Ignoring Important Return Values with PHP 8.5's #[NoDiscard]"

There is a whole class of PHP bug that never shows up in tests, never triggers an exception, and never logs anything. You call a method, the method returns something important, and you just… do not use it. The application continues. Nothing visibly breaks. And then, in production, behavior is subtly wrong because you were looking at stale data or making decisions based on an operation whose result you silently discarded.

PHP 8.5 ships a targeted fix for exactly this: the #[\NoDiscard] attribute. It is a small addition with outsized impact on the quality of the APIs you write and consume.

What the Problem Actually Looks Like

The classic example is immutable objects. If you have ever worked with PSR-7 HTTP message objects, you know this trap well:

$request = $request->withHeader('Content-Type', 'application/json');

PSR-7 messages are immutable. withHeader() does not modify $request in place; it returns a new instance with the header applied. If you write this:

$request->withHeader('Content-Type', 'application/json');
// $request still has no Content-Type header

…you have discarded the only thing that mattered. PHP gives you no warning. The original $request is unchanged. You find out at runtime, probably when a downstream service rejects your request.

The same pattern shows up in builder chains, configuration objects, and any method that follows a fluent interface but returns a mutated copy rather than mutating in place. It also shows up in batch operations that return error lists:

function notifyUsers(array $userIds): array  // returns failed IDs
{
    // ...
}

notifyUsers($batch);  // Failures silently thrown away

The #[\NoDiscard] Attribute

PHP 8.5 introduces #[\NoDiscard] as a built-in attribute you place on any function or method whose return value is important enough to warrant a warning when ignored. When the attribute is present and a caller discards the return value, PHP emits an E_WARNING at runtime.

#[\NoDiscard]
function computeHash(string $data): string
{
    return hash('sha256', $data);
}

computeHash($payload); // Warning: Return value of computeHash() should not be discarded

The attribute accepts an optional message parameter to explain why the return value matters:

#[\NoDiscard("as some items may fail validation")]
function validateBatch(array $items): array
{
    $failures = [];
    foreach ($items as $item) {
        if (! $item->isValid()) {
            $failures[] = $item->id;
        }
    }
    return $failures;
}

When PHP emits the warning for this function, it includes your message: Return value of validateBatch() should not be discarded as some items may fail validation. That context is what turns a generic warning into actionable information for whoever reads it.

What Counts as “Using” the Return Value

Any of these suppress the warning:

$result = validateBatch($items);       // assigned
if (validateBatch($items)) { ... }     // used in expression
$errors = [...validateBatch($items)];  // spread into another structure

If you genuinely need to call a #[\NoDiscard] function and have a legitimate reason to ignore the result, PHP 8.5 introduces a (void) cast for exactly this:

(void) validateBatch($items); // explicit: I know, I'm intentionally discarding this

This communicates intent far better than the pre-8.5 workaround of assigning to $_. The (void) cast makes it clear to future readers that the discard is deliberate, not accidental.

Inheritance and Interfaces

The attribute propagates through inheritance and interface implementations. If an interface method is marked #[\NoDiscard], all implementing classes are covered without needing to repeat the attribute on every concrete implementation:

interface HashProvider
{
    #[\NoDiscard]
    public function hash(string $data): string;
}

class Sha256Provider implements HashProvider
{
    public function hash(string $data): string
    {
        return hash('sha256', $data);
    }
}

$provider = new Sha256Provider();
$provider->hash($payload); // Warning still fires, inherited from interface

This is the behavior you want when designing APIs for others to consume. Annotate the contract once, at the interface level, and the enforcement follows every implementation.

PHPStan Support

PHPStan added full support for #[\NoDiscard] alongside the PHP 8.5 release. That means static analysis can catch these violations before runtime, before code review, and before a single test runs. Catching an ignored return value in your IDE or CI pipeline is dramatically better than finding it when something is subtly wrong in production.

From the PHPStan maintainers: #[\NoDiscard] errors are non-ignorable in their tooling. If you put the attribute on a function, you are declaring that its return value is critical. PHPStan will not let you @phpstan-ignore your way around it. If you do not want the enforcement, you remove the attribute. This keeps the signal honest.

vendor/bin/phpstan analyse
 ------ -----------------------------------------------
  Line   src/Services/UserService.php
 ------ -----------------------------------------------
  42     Return value of validateBatch() is not used.
 ------ -----------------------------------------------

Where to Apply It

The attribute is most valuable in a few specific contexts.

Immutable or “with-er” method chains: Any method that returns a modified copy of the object rather than mutating in place should be marked. This is the pattern behind PSR-7, Carbon’s immutable mode, and PHP 8.4’s readonly class enhancements.

class QueryBuilder
{
    #[\NoDiscard("withTable() returns a new instance, does not mutate")]
    public function withTable(string $table): static
    {
        $clone = clone $this;
        $clone->table = $table;
        return $clone;
    }
}

Functions where the return value is the entire point: Hashing, encoding, and transformation functions are often called without storing the result. If the transform is the reason for the call, mark it.

Batch operations that return failure lists: Notification dispatchers, import pipelines, and bulk update operations frequently return the IDs or items that failed. Discarding that list means discarding your error visibility.

Factory methods and named constructors: create(), from(), make() and similar static constructors that return new instances are an obvious case. Calling a factory without using the result is always a bug.

What It Cannot Be Applied To

The attribute is not valid on functions with void or never return types, and it cannot be placed on property hooks. Both restrictions are logical: you cannot meaningfully warn about discarding a value that does not exist.

Why This Matters for API Design

Adding #[\NoDiscard] to the right methods is a form of machine-readable documentation. It expresses an invariant about how the function must be used, in a way that both PHP’s runtime and static analysis tools can enforce. You are not just writing a comment explaining the method’s behavior; you are embedding the constraint into the language itself.

For library authors, this is significant. If you maintain a package where immutability or return-value consumption is part of the contract, #[\NoDiscard] communicates that contract to every consumer of your library, in every editor, on every CI run. It is the difference between hoping users read your docs and knowing the tools will remind them when they do not.

PHP 8.5 has already shipped with #[\NoDiscard] applied to flock() and a handful of other standard library functions where ignoring the return value is a common source of bugs. That is a good indication of where the core team sees the attribute fitting into the language long-term.

Sources