Every PHP dev has shipped this bug at least once: $date->setDate(2026, 5, 27) on a DateTimeImmutable, then wondered why the date never moved. Or $response->withHeader('X-Trace', $id) followed by return $response — and the header is missing in prod. The language never told you. PHP 8.5's new #[\NoDiscard] attribute does.
The real bug PHP 8.5 NoDiscard catches#
The classic version of this footgun, untouched for a decade:
<?php
$date = new DateTimeImmutable('2026-01-01');
$date->setDate(2026, 5, 27); // returns a NEW DateTimeImmutable
echo $date->format('Y-m-d'); // 2026-01-01 — the original
DateTimeImmutable::setDate() returns a new instance. The original is untouched. Before PHP 8.5 the call above ran silently. On PHP 8.5 you now get:
Warning: The return value of method DateTimeImmutable::setDate() should either be used or
intentionally ignored by casting it to (void)
That is the entire feature in one line: a runtime warning when a marked return value is dropped. The engine emits E_WARNING for native functions and E_USER_WARNING for userland code. It cannot be applied to void or never return types — there is nothing to discard, so the attribute is rejected at compile time.
If you have ever wired up custom Rector rules or PHPStan stubs to catch this, you can delete those rules now — the language carries the contract.
Applying #[NoDiscard] to your own method#
The attribute lives in the global namespace, so you import the FQCN or escape the leading backslash. Here is an immutable query scope builder pattern I use in most Laravel apps:
<?php
namespace App\Support;
final class TenantScope
{
public function __construct(
private readonly int $tenantId,
private readonly array $extraConstraints = [],
) {}
#[\NoDiscard('TenantScope is immutable — assign the returned instance')]
public function withConstraint(string $column, mixed $value): self
{
return new self(
$this->tenantId,
[...$this->extraConstraints, $column => $value],
);
}
}
Now this calling code, which previously did nothing useful, surfaces the bug immediately:
$scope = new TenantScope(42);
$scope->withConstraint('active', true); // E_USER_WARNING with your custom message
The pattern fits anywhere you return a new instance instead of mutating: response builders, Carbon (->addDays()), QueryBuilder clones in custom packages, value objects. If you already lean on readonly classes for value objects in Laravel, #[\NoDiscard] is the runtime backstop for callers who forget the contract.
Make the warning self-explanatory#
The optional message argument is the difference between a useful warning and a confusing one. Compare the default message:
Warning: The return value of method App\Support\TenantScope::withConstraint() should either be used...
…with a custom one:
#[\NoDiscard('TenantScope is immutable — assign $scope = $scope->withConstraint(...)')]
Warning: TenantScope is immutable — assign $scope = $scope->withConstraint(...)
The second tells the developer exactly what to do without leaving their editor. For library code that lands in someone else's IDE without context, this is non-negotiable.
Suppressing intentionally with (void)#
Sometimes you really do want to discard. Maybe you call a marked function purely for its side effect during a teardown, or you are inside a swallow-all-exceptions retry loop. PHP 8.5 added the (void) cast specifically for this:
// Intentional discard — no warning
(void) $response->withHeader('X-Debug-Run', uniqid());
// Single line, same effect
(void) flock($fh, LOCK_UN);
The cast does nothing at runtime. It exists purely to mark the discard as intentional. OPCache will not optimise it away, so dev and prod behave the same — that matters because, as the PHPStan team flagged when shipping 8.5 support, a missing call between environments is far worse than a noisy warning.
What you must not do:
// DON'T — error suppression hides real bugs too
@$response->withHeader('X-Debug-Run', uniqid());
// DON'T — assigning to a throwaway works but reads as a mistake
$_ = $response->withHeader('X-Debug-Run', uniqid());
@ swallows every diagnostic from that expression, not just the NoDiscard warning. If withHeader() later throws because of a malformed value, you will not see it. The (void) cast is precise: it suppresses only this one diagnostic class.
Where core PHP applied it#
The RFC ships the attribute on two long-standing offenders. Both have shipped bugs in production code I have reviewed:
// flock() — ignoring the return masks a failed lock
$fh = fopen('/var/lock/import.lock', 'w');
flock($fh, LOCK_EX); // 8.5: E_WARNING — false return means no lock held
// DateTimeImmutable::set*() — every setter returns a new instance
$start = new DateTimeImmutable('2026-01-01');
$start->setTime(9, 0); // 8.5: E_WARNING — $start is unchanged
$start->setDate(2026, 6, 1);
$start->setTimezone(new DateTimeZone('UTC'));
$start->modify('+1 day');
The flock() decision was actively debated on the php-src issue tracker because there are legitimate cases for discarding it. Either way, the (void) escape hatch handles them.
Expect the standard library to pick up more of these over time. If you write packages, audit your public API for methods named with*, set*, add*, or to* that return self or static — those are your candidates.
Laravel patterns that benefit#
A few real spots in a Laravel app where I now apply it:
<?php
namespace App\Http\Responses;
final class ApiResponse
{
public function __construct(
private readonly array $data,
private readonly array $headers = [],
) {}
#[\NoDiscard('ApiResponse is immutable — assign $r = $r->withHeader(...)')]
public function withHeader(string $name, string $value): self
{
return new self($this->data, [...$this->headers, $name => $value]);
}
#[\NoDiscard('ApiResponse is immutable — assign $r = $r->withData(...)')]
public function withData(array $data): self
{
return new self([...$this->data, ...$data], $this->headers);
}
}
Custom Eloquent scope objects, Pipeline::send() returning a configured pipeline, custom Carbon macros that build new instances — all benefit. The pattern compounds with PHP 8.5's clone with syntax for updating readonly objects: the language now both encourages immutable builders and warns when you misuse them.
If you build your own attributes elsewhere in the app, the practical guide to PHP custom attributes beyond Laravel's built-ins covers the reflection patterns that make them composable with #[\NoDiscard].
Gotchas and edge cases#
A few things I tripped over on the first pass:
The warning fires on the call expression, not the line. So if you wrap the call in a ternary or a match arm and never bind the value, you still get the warning — even if the branch is never taken at runtime. Refactor to a named variable when you need conditional usage.
Reflection cannot strip the attribute. There is no setAttributes() — once your dependency declares #[\NoDiscard], callers either use the return value or (void) it. If a library applies it too aggressively, file an issue rather than monkey-patching.
The warning is fully ignorable at the PHP level (it is E_WARNING, not E_ERROR), but PHPStan treats #[\NoDiscard] violations as non-ignorable. That is intentional: a missed return value is almost always a bug, and the noise saves real prod incidents. If you are not yet running PHPStan at level 10 in your Laravel apps, this is one more reason to climb the ladder.
Constructors, magic methods (__call, __invoke), and abstract methods all accept the attribute. Trait methods inherit it. Interface methods do not propagate the attribute to implementations automatically — you must repeat it on each implementation if you want consistent enforcement.
Wrapping up#
Add #[\NoDiscard] to any method that returns self or static and is meant to be immutable. Always supply a custom message. Suppress with (void) and only (void). The whole feature is ten lines of API surface that quietly removes a class of bug that has shipped silently for years.
If you are upgrading a codebase to 8.5, pair this with the PHP 8.5 deprecations cheat sheet for a one-pass audit, and look at where you might tighten up Laravel attribute usage on models, jobs, and commands at the same time.
FAQ#
What does the #[NoDiscard] attribute do in PHP 8.5?
#[\NoDiscard] marks a function or method's return value as important. When a caller invokes the function and does not use the return value — no assignment, no expression, no (void) cast — PHP emits an E_WARNING for native functions and E_USER_WARNING for userland ones. It is a runtime safeguard against the common bug of treating an immutable builder method as if it mutated its receiver.
Which core PHP functions use #[NoDiscard]?
PHP 8.5 ships the attribute applied to flock() and to the immutable setters on DateTimeImmutable — setDate(), setTime(), setTimezone(), setTimestamp(), and modify(). These are the long-standing footguns where ignoring the return value silently broke code. More core functions are expected to adopt the attribute in 8.6 and beyond as the standard library is audited.
How do I suppress a NoDiscard warning when I genuinely want to discard the value?
Use the new (void) cast: (void) flock($fh, LOCK_UN);. The cast does nothing at runtime, but it tells the engine — and any developer reading the code — that you are intentionally discarding the return. Do not use the @ error suppression operator; it hides every diagnostic from that expression, not just the NoDiscard warning, which masks real bugs.
Can I add #[NoDiscard] to my own Laravel methods?
Yes, and you should. Any method that returns a new instance instead of mutating its receiver is a candidate: custom response builders, immutable value objects, scope builders, Carbon macros that return new instances. Add #[\NoDiscard('Use the returned instance')] and PHP will warn callers who drop the result. Constructors, magic methods, and trait methods all accept the attribute.
Does PHPStan or Psalm support #[NoDiscard]?
PHPStan added full support for #[\NoDiscard] in its PHP 8.5 release and treats violations as non-ignorable errors at every level. It also flags unnecessary (void) casts when the underlying function is not marked. Psalm supports PHP 8 attributes generally and is expected to add specific handling for #[\NoDiscard], but as of writing the dedicated rule is not yet shipped — check the changelog before relying on it.
What is the difference between #[NoDiscard] and #[Deprecated]?
#[\Deprecated] (also new in PHP 8.5) warns when a function or method is called at all — the API itself is being phased out. #[\NoDiscard] only warns when the call happens but the return value is dropped — the API is fine, the calling pattern is not. They are independent and can be combined on the same method, though that is rare in practice.