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

Reply via email to