For about four months one of our admin dashboards said "page 1 of 0" whenever a search came back empty. Nobody filed a ticket. The line responsible was max(1, min($lastPage, $requestedPage)), which hands you 1 when $lastPage is 0 and then gets on with its day. Nothing was ever technically broken. It was just a bit wrong, and wrong quietly, so we never noticed. PHP 8.6.0 RC3 was tagged on 8 October and general availability is planned for 19 November. Having read through what changed, my position is that the most valuable thing in this release is a temperament, not a syntax feature. The language is getting louder about bad input, and every new exception you hit during the upgrade is a bug report you didn't have to write yourself.

Look at the new clamp() function. It would make a neat one-liner for my dashboard, but it refuses to play along when $min is bigger than $max, or when either bound is NAN: it throws a ValueError. In my case that is exactly the empty-result situation, so the bug I lived with for months would have shown up the first time someone searched for gibberish. The same mood runs through the standard library. Plenty of built-in functions that used to shrug at a nonsense flag or an out-of-range number now throw a ValueError. preg_grep() returns false when the regex engine fails during execution, where before it acted as though nothing had happened. base_convert() and its siblings raise a notice when they can't give you a precise answer. And a return inside a finally block is now deprecated, because it has always quietly thrown away whatever exception was on its way out.

Even the shiny new syntax follows the rule. Partial function application was accepted unanimously, and a follow-up RFC tightened it before release: every ? placeholder creates a required parameter, even when the parameter underneath has a default. Write round(?, ?) and you get a closure that insists on both the number and the precision, although round() itself is fine without the second one. If you want the defaults to survive, use ..., which keeps optional parameters optional. I like this decision a lot. A question mark in a call should mean "I will give you a value here", and it shouldn't sometimes mean "or maybe not".

Now the strongest argument against all this, and I take it seriously. From where your users sit, a page that shows a slightly odd number is better than a page that shows an error. A lot of production PHP has been tuned for years, mostly by accident, around the old forgiving behaviour. Shops with thin test coverage won't hear about these new exceptions in CI. They'll hear about them from customers, during a deploy, on a Friday. Turning hidden mistakes into visible failures is fine in principle. In practice it moves the cost onto whoever happens to be on call.

I still land on the loud side, and part of the reason is how carefully core went about it. Stream errors can now be raised as StreamException, which you set up through an error_mode option in the stream context, but the default stays warnings. Nobody's file_get_contents() suddenly throws unless they ask for it. The deprecations only emit E_DEPRECATED, so your code keeps running. And when a deprecation looked like more churn than it was worth, the vote said no: list() and the _() gettext alias are both staying. That isn't a team breaking things for the sake of purity. And the cost difference is real. An exception that blows up in staging costs you a coffee and a small patch. A silent wrong value can cost you a year of bad rows that you then have to reconcile by hand. RC4 and RC5 are still on the calendar, so there's time to point a CI job at the release candidate and let it complain.

Here is the twist that bothers me. The one new feature that fails silently is partial function application, which is the same feature everyone is excited about. Bound arguments are evaluated once, at the moment the partial is created, and the closure stores the results. Picture a long-running queue worker that builds a logging partial at boot and fills in date('c') as one of the arguments. Every job it processes six hours later still carries the boot timestamp. You won't get an error, a notice or a deprecation. An arrow function would have called date() on every invocation. Go developers spent years with a similar capture gotcha in their loop variables before the language changed it, so we are in good company. But in a release that is otherwise so keen to speak up, this is the place where nothing tells you anything, and I would put a review rule or a static analysis check on any partial whose bound arguments contain a function call.

So here is my question for you. Where should the line be drawn? Should a future version, or your tooling, warn when a partial binds something that isn't a constant or a plain variable? Or is that the kind of hand-holding you'd switch off on day two? And if you've got code where a silent fallback is actually holding things up, where a thrown ValueError would hurt more than the wrong answer it replaces, tell me about it in the comments, because that's the strongest case against my argument and I'd like to see it.