Skip to content

String.replace(regex, '$1') returns '[object Object]' instead of captured-group interpolation #141

@proggeramlug

Description

@proggeramlug

Summary

String.prototype.replace(regex, replacement) returns [object Object] instead of a string whenever the replacement contains a backreference like $1. typeof the result is "object", not "string". Works correctly in Node/V8.

Repro

const s = "abc123";
const out = s.replace(/^"(.*)"$/, "$1");  // pattern doesn't even match here
console.log("out=" + out);                 // prints: out=[object Object]
console.log("typeof=" + typeof out);       // prints: typeof=object

Compile and run:

perry compile repro.ts -o repro
./repro

Expected vs actual

out=abc123        // JS semantics: pattern doesn't match, original string returned
typeof=string

Actual:

out=[object Object]
typeof=object

Impact

This silently breaks any code that uses .replace(regex, "$1") (or $2, $&, etc.) to strip wrappers, extract submatches, normalize values, etc. Downstream code then gets [object Object] wherever a string was expected — no error thrown, so the failure surfaces far from the root cause.

Real-world hit: perry-hub's multipart parser does boundaryMatch[1].replace(/^"(.*)"$/, '$1') to strip optional quotes from a Content-Type boundary. The delimiter becomes --[object Object], nothing splits, every POST /api/v1/build returns Missing 'manifest' field regardless of the actual payload. Patched in the hub by dropping the backreference and using explicit startsWith/endsWith + substring instead.

Minimal workaround

// Instead of: s.replace(/^"(.*)"$/, "$1")
let out = s;
if (out.startsWith('"') && out.endsWith('"') && out.length >= 2) {
  out = out.substring(1, out.length - 1);
}

For generic capture-group extraction, use .match()[n] directly — match() with capture groups works (just verified: m[1] returns the captured string with typeof === "string"). It's the replacement-string $n / $& path that's broken.

Other replacement forms — not tested yet

Also worth sanity-checking whether .replace(regex, (match, p1) => ...) (function replacer) works, and whether $& / $' / $ / $$ are similarly broken. I haven't tested those — if any of those also return [object Object], it points to a single shared codepath for replacement-string interpolation.

Environment

  • perry version: 0.5.164 (also observed at 0.5.162, 0.5.158)
  • Platform-independent — codegen/runtime bug.

Suggested compile-time warning

Same pattern as the follow-up to #136: when .replace(regex, string) is called and the string literal contains $n / $& / $' / $` / $$, either make it work or emit a compile-time error until it does. Today it's a silent no-op that produces [object Object] and no diagnostic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions