skip to content

In PHP, what do preg_match() and preg_match_all() return, and why should code extracting order references compare the result with === false?

level: middleimportance: must knowfreq 60%

answer

  1. 1, 0 or false
  2. preg_match stops at the first match
  3. preg_match_all returns a count
  4. false means the engine failed
  5. preg_last_error_msg since PHP 8.0

basics

~20 s

preg_match() returns 1 for a match, 0 for none and false when the regex fails; preg_match_all() returns the number of matches or false. A truthy if merges 0 and false, so an engine error looks like 'no order reference found'.

solid answer

~50 s

`preg_match(string $pattern, string $subject, &$matches = null, int $flags = 0, int $offset = 0): int|false` stops at the first match and returns `1`, returns `0` when nothing matches, and returns `false` when matching could not be performed: an invalid pattern (which also emits a warning), invalid UTF-8 under the `u` modifier, or an exhausted backtrack limit (which emit nothing). `preg_match_all()` returns the number of full matches, which may be `0`, or `false`. The captures land in `$matches`: index `0` is the whole match, `1` and up are groups, and named groups also appear under their names. Code written as `if (preg_match(...))` treats a failure exactly like a miss, so a support email whose reference the regex could not scan is silently filed as having none. Compare with `=== false` first and report `preg_last_error_msg()`, added in PHP 8.0.

code

php · 20 lines
php
<?php
declare(strict_types=1);

$body = 'My order ORD-2026-000123 arrived damaged; see also ORD-2026-000456.';
$pattern = '/\bORD-(?<year>\d{4})-(?<seq>\d{6})\b/';

$found = preg_match($pattern, $body, $m);
if ($found === false) {
    throw new RuntimeException('Regex failed: ' . preg_last_error_msg());
}
if ($found === 1) {
    echo $m[0], ' ', $m['seq'], "\n";      // ORD-2026-000123 000123
}

$count = preg_match_all($pattern, $body, $all);
echo $count, "\n";                          // 2
echo implode(', ', $all[0]), "\n";          // ORD-2026-000123, ORD-2026-000456

var_dump(preg_match('/ORD/u', "ORD \xC3"));  // bool(false): invalid UTF-8 under u
echo preg_last_error_msg(), "\n";           // Malformed UTF-8 characters, possibly incorrectly encoded

go deeper

for a junior

Recall that preg_match returns 1 or 0 and fills $matches, with $matches[0] as the full match and later indexes as groups.

for a middle

Explain the third outcome, false, what causes it, which failures warn and which stay silent, and how preg_last_error_msg reports them.

for a senior

Show you would branch strictly on false in every extraction path, log the error code with an input identifier, and test the failure branch deliberately.

for a principal

Treat silent regex failure as a data-loss risk in pipelines and set a team rule, enforced by static analysis, that preg_* results are never used in a plain boolean test.

## Three outcomes, not two The `preg_*` functions run **PCRE2** patterns. `preg_match()` has this signature: ```php preg_match(string $pattern, string $subject, &$matches = null, int $flags = 0, int $offset = 0): int|false ``` It searches for the **first** match and returns: | Return | Meaning | |---|---| | `1` | the pattern matched; `$matches` is filled | | `0` | the pattern did not match | | `false` | matching failed; the result says nothing about the subject | `preg_match_all()` has the same parameters but keeps searching and returns the **number of full matches**, from `0` upward, or `false` on failure. ## What makes them return false `false` comes from two different kinds of problem, and they behave differently: 1. **The pattern cannot be compiled**: a missing delimiter, an unknown modifier, a syntax error. PHP emits an `E_WARNING` such as `Compilation failed: ...` and returns `false`. 2. **The match cannot be completed**: the subject is invalid UTF-8 while the `u` modifier is set, or the engine exceeded `pcre.backtrack_limit`, `pcre.recursion_limit` or the JIT stack. No warning is emitted; the only trace is the error code. The second kind is the dangerous one, because it depends on the **input**. A pattern that works on every test email can fail on one long or malformed message in production. ## Reading the error - `preg_last_error(): int` returns a constant such as `PREG_NO_ERROR`, `PREG_INTERNAL_ERROR`, `PREG_BACKTRACK_LIMIT_ERROR`, `PREG_RECURSION_LIMIT_ERROR`, `PREG_BAD_UTF8_ERROR`, `PREG_BAD_UTF8_OFFSET_ERROR` or `PREG_JIT_STACKLIMIT_ERROR`. - `preg_last_error_msg(): string`, added in **PHP 8.0**, returns the matching text, for example `Backtrack limit exhausted` or `Malformed UTF-8 characters, possibly incorrectly encoded`. Both describe the **last** `preg_*` call, so read them immediately. ## The truthiness trap In a support-mail pipeline the naive code is: ```php if (preg_match('/\bORD-\d{4}-\d{6}\b/', $body, $m)) { attachToOrder($m[0]); } ``` `0` and `false` are both falsy, so a failed scan and a mail without a reference take the same branch. The ticket silently lands in the "no order" queue, and nothing in the logs explains why. The fix is to branch on all three outcomes: - `=== false`: raise or log with `preg_last_error_msg()`; - `=== 0`: genuinely no reference; - `=== 1`: use `$matches`. ## The $matches array For `preg_match()`: - `$matches[0]` is the text matched by the whole pattern; - `$matches[1]`, `$matches[2]` and so on are the capture groups in order; - a named group `(?<seq>\d{6})` appears under both its name and its number; - `PREG_OFFSET_CAPTURE` turns every entry into a `[text, byteOffset]` pair; - `PREG_UNMATCHED_AS_NULL` reports groups that did not participate as `null`. For `preg_match_all()` the default layout, `PREG_PATTERN_ORDER`, gives `$matches[0]` as the list of all full matches and `$matches[1]` as the list of first groups; `PREG_SET_ORDER` groups each match's captures together instead. `$matches` is overwritten by every call, including one that returns `0`, so never read a previous call's captures after a miss. ## A checklist for extraction code 1. Anchor the reference with `\b` or explicit context so `XORD-...` is not accepted. 2. Compare the return value strictly and handle `false`. 3. Log the error message together with an identifier of the input, not the whole email. 4. Cover the failure branch with a test that feeds invalid UTF-8 to a `u` pattern, which reliably produces `false`. ## What interviewers listen for Most candidates know that `preg_match()` returns `1` or `0`. The question is really about the third value. A strong answer: - names `false` and the two kinds of cause, compile-time and runtime; - says which of them warn and which are silent; - shows the three-way branch and the use of `preg_last_error_msg()`; - explains that `preg_match_all()` returns a count, so `0` is a normal answer there too. Candidates who add that a silent regex failure in a pipeline is a data-quality bug, not just a code smell, show they have been paged for one.

  • Does preg_match() emit a warning when it returns false?
    Only for pattern problems: a missing delimiter, an unknown modifier or a compilation error produce an `E_WARNING`. Runtime failures, such as invalid UTF-8 under `u` or an exhausted backtrack limit, emit nothing and only set the code returned by `preg_last_error()`, which is why the return value must be checked.
  • What does preg_match_all() return when the subject contains three order references?
    It returns `3`, the number of full matches, and fills `$matches` in `PREG_PATTERN_ORDER` by default, so `$matches[0]` holds the three references. It returns `0` when there are none and `false` when matching failed.
  • Why can preg_match() fail on one email but not on another with the same pattern?
    Because runtime failures depend on the subject: invalid UTF-8 only matters when the text contains it, and the backtrack and JIT stack limits are hit only by inputs that make the engine do enough work, often long bodies with near-matches.

saying these in an interview costs you the question

  • preg_match returns true or false like any boolean test.
  • preg_match returns the number of matches in the whole string.
  • if (preg_match(...)) is enough because errors always throw an exception.
  • A failed match always emits a warning, so errors will show up in the logs.
  • $matches keeps its previous contents when preg_match returns 0.