Hi Tim,
Am 04.09.2026 um 19:51 schrieb Tim Düsterhus:
2. I disagree with the behavior of not wrapping Exceptions thrown in
user callbacks: I believe the correct choice is to throw a
\PregException with the Exception thrown in the callback as the
`->previous` exception. Not wrapping the user callback exception means
that one needs a `catch(Exception)` with a try just around the preg_
call to reliably handle all errors during regular expression
execution, which nullifies much of the benefit of having a dedicated
exception class in the first place.
It also violates the exception policy in
https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables,
which states:
If an extension uses external functionality that may throw an
exception it MUST wrap any exception thrown by that functionality
into an appropriate exception of its own. It MUST set the $previous
property to the original exception when doing so.
Following the principle of least surprise, i would expect that this code:
try {
preg_replace_callback($pattern, $callback, $subject);
} catch (ValidationException $e) {
// Handle it
}
can be changed to the following, and still work like expected:
try {
preg_replace_callback($pattern, $callback, $subject, flags:
PREG_THROW_ON_ERROR);
} catch (ValidationException $e) {
// Handle it
}
When PREG wraps everything thrown in a user supplied callback into an
PregException, above code would no longer work.
Instead, we would need this:
try {
preg_replace_callback($pattern, $callback, $subject, flags:
PREG_THROW_ON_ERROR);
} catch (PregException $e) {
if ($e->getPrevious() instanceof ValidationException) {
// Handle it
} else {
throw $e; // Rethrow if it wasn't the one we expected
}
}
The policy states: "If an extension uses external functionality"
I would argue, that this should only target exceptions from the
extension's own external dependencies. As a user i do not care about
internal implementation details of an extension. In fact, it would be
surprising to the caller to face foreign exceptions. Therefore it makes
sense for any extension to catch those and wrap them in their own
exception hierarchy.
However, i would not regard user supplied callbacks to fall under that
policy rule. The extension itself is not using that code and knows
nothing about it. It just calls it, following the "Inversion of Control"
principle back into userland.
As always, a good indicator for questions like this is to check what
other languages are doing.
In Javascript, the Exception propagates untouched:
class MyError extends Error {}
const text = "test";
try {
text.replace(/.+/g, (match) => {
throw new MyError("User error");
});
} catch (error) {
console.log(error instanceof MyError); // true
}
Same in Python:
import re
def my_callback(match):
raise ValueError("User exception")
try:
re.sub(r'\d+', my_callback, "test")
except ValueError as e:
print("Caught user exception")
And while i didn't confirm it by running code myself, my research
pointed to Java, C#, C++, Rust and Ruby doing the same thing.
So it seems to me, it is standard convention in most popular languages
to let user exceptions propagate transparent.
PHP would be an outlier, if it started wrapping them inside
PregException. I don't think this would be the right call.
Regards,
Sascha
--
*Freitags habe ich immer frei: Wir arbeiten bei F&P in einer
4-Tage-Woche. *#32istdasneue40**
F&P ♡ Creating Communities
<https://www.fp.de>
<https://www.linkedin.com/company/f&p-gmbh-feig-&-partner/>
<https://www.instagram.com/fp_creating_communities/>
<https://www.kununu.com/de/fundp>
F&P GmbH
Feldstraße 53
95152 Selbitz
Tel. 09280 - 98 11 18 - 0
Sitz der Gesellschaft: Feldstraße 53, 95152
Selbitz
Geschäftsführer: Dr. Ingmar Ackermann, Frank Noack
Amtsgericht Hof,
HRB 3352