chrome-extension://regex-tester-pro · live preview

Pattern

/(\d+)\s*USD/g

Test string

Total: 99 USD before tax.

flags: g · 1 match · 0.4 ms

All matching happens in your browser. Patterns and test strings never leave the tab.

Regex Guide

Regex Replace With a Function in JavaScript: Patterns, Pitfalls, Speed

Updated May 2026 8 min read By the Regex Tester Pro team

Quick answer

Pass a function as the second argument to replace() when the replacement depends on the matched content. The callback receives the full match, then each capture group, then the offset, then the input string, then a groups object if you used named groups. Return a string; it replaces the match.

String replacement is one of the most common reasons developers reach for regex. Using a function instead of a static string unlocks transformations that would otherwise need two passes. Here is the full signature, the gotchas, and when to prefer a callback over a string.

JavaScript
// Convert all USD prices to CHF, computed per-match
const usdToChf = 0.89;
const text = 'Total: 99 USD before tax. Shipping 15 USD extra.';

const out = text.replace(/(\d+)\s*USD/g, (full, amount) => {
  const chf = (Number(amount) * usdToChf).toFixed(2);
  return chf + ' CHF';
});

console.log(out);
// 'Total: 88.11 CHF before tax. Shipping 13.35 CHF extra.'

// With named groups, use the last argument:
text.replace(/(?<n>\d+)\s*USD/g, (_full, _n, _off, _src, groups) => {
  return (Number(groups.n) * usdToChf).toFixed(2) + ' CHF';
});

The full callback signature

When you pass a function to replace(), JavaScript invokes it for every match (with /g) or just once (without). The arguments, in order:

  1. match: the full matched substring
  2. p1, p2, ...: each capture group (one argument per group)
  3. offset: the zero-based index of the match in the input
  4. string: the entire input string
  5. groups: an object with named groups (if any)

The return value is converted to string and substituted for the match. undefined becomes the literal string undefined, which is rarely what you want, so always return an explicit string.

When to use a function over a string

Static replacement strings cover most cases: '$1-$2', '$<name>', plain text. Reach for a function when:

If your function is a one-liner that just rearranges captures, the string form is faster and more readable. If you find yourself reaching for a switch, a function is the right tool.

Async traps

The callback is called synchronously. Returning a Promise gives you the literal string "[object Promise]" replacing every match. There is no built-in replaceAsync(), although the pattern is straightforward:

The two-pass form is always preferable to fighting the synchronous callback. It also gives you parallelism, which a sequential async loop would not.

Performance, plain strings vs functions

On V8, with a typical pattern and a million matches:

For under 100,000 matches per call, the difference is irrelevant. For tight inner loops on a million matches per second, the function form has a real cost. The fix, if you really need it, is to compile the transformation table once and use the function only when the lookup misses.

Named groups in the callback

If your regex uses named groups, the groups object is passed as the last argument:

Some teams adopt the convention of always destructuring the last argument when the regex has named groups, since it makes the intent obvious to readers. Once you mix positional and named, the function signature can grow long; named groups plus the trailing groups object is the cleanest pattern.

Common pitfalls

Three to watch for:

  1. Forgetting /g. Without the global flag, the function runs once. Easy to miss when you copy a regex from somewhere.
  2. Mutating outside state during the replacement. It works, but it makes the function order-dependent. If you only need a count, do a matchAll() first and replace separately.
  3. Returning non-string values. The engine coerces to string. Numbers become digits, objects become "[object Object]". If your callback might return a non-string, wrap it in String().

Used carefully, the callback form of replace() is one of the most expressive tools in JavaScript's regex API. It is also one of the most overlooked.

Prototype your replacement function live

Regex Tester Pro gives you live matching, named groups, replacements, and reference cheatsheets in one Chrome popup. Free, private, no signup.

Add to Chrome, free

Frequently asked questions

Does the callback run for non-matches?
No. It runs only for matches. Non-matching parts of the input pass through unchanged.
Can I skip a replacement conditionally?
Return the full match unchanged: (m) => shouldSkip(m) ? m : transform(m). The replacement equals the original text, so no visible change.
Does the function form work with replaceAll()?
Yes. String.prototype.replaceAll() takes the same callback signature as replace() with /g, and it works on plain strings too.
What is the difference between $1 in a string and the second argument in a function?
They reference the same capture group. The string form does the substitution syntactically; the function form gives you the value to compute with.
Can the callback be async?
No. replace() is synchronous and discards Promise return values as string "[object Promise]". Pre-compute async data, then run a sync replace.

More by Peak Productivity

Free developer tools, made for the browser

Privacy-first utilities that run locally. No upload, no signup, no watermark.