---
title: "Discover PHP 8.6 Through Its New Error Messages"
url: https://www.exakat.io/discover-php-8-6-through-its-new-error-messages/
date: 2026-10-05
modified: 2026-10-05
lang: en
author: "dams"
description: "Discover PHP 8.6 Through Its New Error Messages A guided tour of what will bark at your code when you move, and how you should care now PHP 8.6 lands..."
categories:
  - "Technology"
tags:
  - "error messages"
  - "PHP 8.6"
image: https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640.jpg
word_count: 1761
---

# Discover PHP 8.6 Through Its New Error Messages

# Discover PHP 8.6 Through Its New Error Messages

*A guided tour of what will bark at your code when you move, and how you should care now*

PHP 8.6 lands in November 2025. You have a few weeks left to run your test suite on the release candidate, spot the deprecation notices, and fix them while nobody watches. Migrating now costs you an afternoon, and even if partially applied, it might improve your code anyway.

With the introduction of new features come error messages: after all, without experience yet, you might very well trespass the extend of the features. So, let's see these new possible error messages, so you can react faster when you encounter them. If you ever do.

## No more magic numbers: use the constants

PHP has always shipped named constants for function arguments. It has also always accepted raw integers instead, without a single complaint. That free ride stops in PHP 8.6 for a growing list of functions.

Take `pathinfo()`. Before 8.6, you could combine `PATHINFO_DIRNAME | PATHINFO_EXTENSION` and get... one of them. Which one? The answer depended on internal flag values abd black magic, not on logic. Now PHP throws a `ValueError`:
> [pathinfo(): Argument #2 ($flags) must be only one of the PATHINFO_* constants](https://php-errors.readthedocs.io/en/latest/messages/must-be-only-one-of-the-pathinfo_*-constants.html)
The same treatment applies to `array_filter()`. Passing `42` as the mode used to default silently to `ARRAY_FILTER_USE_VALUE`. Now it rages at you:
> [arrayfilter(): Argument #3 ($mode) must be one of ARRAY_FILTER_USE_VALUE, ARRAY_FILTER_USE_KEY, or ARRAY_FILTER_USE_BOTH](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-array_filter_use_value,-array_filter_use_key,-or-array_filter_use_both.html)
And `scandir()` follows the same path, refusing invented sorting orders:
> [scandir(): Argument #2 ($sortingorder) must be one of the SCANDIR_SORT](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)[_](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)ASCENDING, SCANDIR[_](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)[SORT](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)[_](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)DESCENDING, or SCANDIR[_](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)[SORT_NONE constants](https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html)
This trend deserves attention. PHP keeps replacing silently-accepted literal values with strict constant checks. Each time, the old behavior was to pick a default and hope nobody notices. The new behavior tells you exactly what went wrong and what to use instead. If you grep your codebase for raw integers passed to these functions, fix them now. The constants existed all along; PHP just stopped being polite about ignoring them.

## Partial application arrives with guardrails

PHP 8.6 introduces partial function application, one of the most anticipated features in years. You can write `add(1, ?)` and get a `Closure` that remembers the `1` and waits for the rest. It is elegant, concise, and is also guarded by two brand-new error messages.

The first fires when you supply too many arguments during the partial call, just like any other function call, but with a reminder that you are using PFA:
> [Partial application of add() expects at most 2 arguments, 4 given](https://php-errors.readthedocs.io/en/latest/messages/partial-application-of-%25s()-expects-at-most-%25d-arguments,-%25d-given.html)

And a partial application cannot sneak extra arguments past the function signature. The `?` placeholder reserves a slot for later; it does not magically create new parameters. What would they be used for? The body of the function is not changed by PFA, so they would be lost in translation.

The second fires when you supply too few:
> [Partial application of add() expects exactly 3 arguments, 2 given](https://php-errors.readthedocs.io/en/latest/messages/partial-application-of-%25s()-expects-%25s-%25d-arguments,-%25d-given.html)

Every required parameter needs either a value or a `?` placeholder. If you want to defer all remaining parameters at once, use `...` instead: `add(1, ...)` captures everything still missing. PHP checks the argument count at partial-creation time, not at call time. Better to fail early than to hand you a `Closure` that would blow up the moment you try to use it.

## Constructors and destructors get their act cleaned up

Two long-standing PHP oddities finally get the deprecation notice they deserved for a decade. Until now, they were mostly reported by static analysis.

First, the generator constructor. Yes, you could put `yield` inside `__construct()`. Yes, it compiled. No, it never actually ran as a generator, because a generator only starts executing when iterated, and nothing iterates a constructor. The object was born with its initialization silently skipped.
> [Making a constructor a Generator is deprecated](https://php-errors.readthedocs.io/en/latest/messages/making-a-constructor-a-generator-is-deprecated.html)

Who thought this was a good idea? Nobody did. The feature emerged as a side effect of how generators work: any function with `yield` becomes a generator, and `__construct()` was no exception. PHP 8.6 recognizes this as an elephpant trap. The same deprecation applies to destructors, by the way. Geez...

Second, returning a value from a constructor. The `new` keyword always returns the fresh object, regardless of what `__construct()` hands back. Any `return $value` inside a constructor was dead code wearing a disguise:
> [Returning a value from a constructor is deprecated](https://php-errors.readthedocs.io/en/latest/messages/returning-a-value-from-a-constructor-is-deprecated.html)
A bare `return;` to exit the constructor early remains perfectly fine. Only `return $something;`triggers the deprecation, because that `$something` vanishes into the void every single time. And `$something` could have been as nothing as `null`.

## Returning from finally, a silent killer

Here lies one of the most treacherous patterns in PHP. A `return` inside a `finally` block silently replaces whatever the `try` or `catch` was about to return. Worse, it swallows any exception that was propagating, without a trace.
> [Returning from a finally block is deprecated](https://php-errors.readthedocs.io/en/latest/messages/returning-from-a-finally-block-is-deprecated.html)

PHP already forbids `break`, `continue`, and `goto` out of a `finally` block. A `throw` inside `finally` at least chains the discarded exception as `$previous`. But `return` got a free pass all these years, quietly eating exceptions and return values with no indication anything happened. That era ends now. Move the `return` out of the `finally` block, and make the control flow visible.

## Named variadics versus internal functions

Internal functions written in C handle their variadic parameters differently from user-land functions. When you spread an associative array into `array_merge()`, PHP tries to match each string key to a parameter name. Since internal variadic functions only have one name for their catch-all parameter, every other key gets rejected:
> [Internal function array_merge() does not accept named variadic arguments](https://php-errors.readthedocs.io/en/latest/messages/internal-function-%25s%25s%25s()-does-not-accept-named-variadic-arguments.html)

The fix takes one line: wrap the array in `array_values()` before spreading it. This strips the string keys and turns them into plain numeric indexes, which the variadic parameter happily accepts. If your data comes from a database query or a JSON decode, string keys might lurk where you expected none. Apply `array_values()` as a habit, just in case.

## NAN walks into clamp

PHP 8.6 ships the new `clamp()` function, which restricts a value to a given range. Simple, useful, and immediately confronted with the eternal troublemaker of floating-point arithmetic: `NAN`.
> [clamp(): Argument #2 ($min) must not be NAN](https://php-errors.readthedocs.io/en/latest/messages/must-not-be-nan.html)

The fundamental problem with `NAN` is that any comparison involving it returns `false`. Ask "is 5 greater than NAN?" and the answer is no. Ask "is 5 less than NAN?" and the answer is also no. A clamping function built on comparisons cannot do anything meaningful with a bound that defeats all comparisons.

Note that `NAN` is technically a float. It passes type checks, it slips through type declarations, and it arrives in your code disguised as a perfectly normal number. The `range()` function already rejects it with "must be a finite number, NAN provided". Now `clamp()` does the same. Validate your floats with `is_nan()` when they come from untrusted sources.

Note that `range()` had a similar problem with `NAN`, and `INF` for that matter. You could get that nice error message:

So, PHP is able to handle infinite numbers. That should attract attention of mathematicians, I guess.

## Virtual properties resist lazy introspection

PHP 8.4 introduced property hooks and lazy objects. PHP 8.6 discovers that these two features do not always get along.

A virtual property, that is declared with only a `get` hook and no backing storage, has no memory slot for storage. The lazy objects API, specifically `ReflectionProperty::setRawValueWithoutLazyInitialization()`, tries to write directly to that memory slot. When the slot does not exist, the method has nothing to write to:
> [Cannot use setRawValueWithoutLazyInitialization() on virtual property Foo::$bar](https://php-errors.readthedocs.io/en/latest/messages/cannot-use-%25s()-on-virtual-property-%25ps::$%25ps.html)

Only properties with actual backing storage, those whose hooks reference `$this->propName`, support raw value manipulation. Virtual properties compute their value on the fly and have nothing to bypass. If your lazy-object initialization code touches property hooks, check which properties are virtual before calling the raw setter.

## The readonly function reaches end of life

When PHP 8.1 introduced `readonly` as a property modifier, the team carved out a compatibility exception: a global function literally named `readonly()` could still be declared and called, because WordPress shipped one. That exception kept the peace for four versions. In PHP 8.6, the moratorium ends:
> [Calling a function "readonly" is deprecated](https://php-errors.readthedocs.io/en/latest/messages/calling-a-function-%22readonly%22-is-deprecated.html)

The deprecation notice fires on every call, not on the declaration. Declaring the function still works, for now: it is only a deprecation. And here is the fun part: `readonly` as a method name remains perfectly legal. You can call `$obj->readonly()` all day long without a single warning. The deprecation targets only bare function calls, as part of the broader effort to fully reserve `readonly` as a language keyword.

If you maintain a library that defines a `readonly()` function, rename it. Pick `isReadonly()`, pick `asReadonly()`, pick anything that will not collide with the language in PHP 9.0.

## The takeaway

PHP 8.6 cleans house. It replaces silent fallbacks with explicit errors, deprecates patterns that never worked as intended, and sets boundaries around its newest features. None of these changes will surprise you if you run your test suite against the RC today.

Every error message mentioned in this article links to its full documentation at [php-errors.readthedocs.io](https://php-errors.readthedocs.io/en/latest/), with code samples, explanations, and migration advice. When PHP hands you a new error message, look it up there. The answer already exists.

Go test your code. November gets closer every day.