InterviewsVector
beginnerRare4 min read · Updated Jul 18, 2026

Implement String.prototype.trim from Scratch

Implement String.prototype.trim in JavaScript with the full Unicode whitespace set, two-pointer scanning, and the catastrophic-regex pitfall interviewers listen for.


The problem

Implement trim(str) — strip leading and trailing whitespace — without using the built-in trim.

It looks like a warm-up, and it is — but it has two teeth hidden in it:

  1. "Whitespace" is bigger than you think. The spec's set is not ' ', '\t', '\n', '\r' — it includes \v, \f, the no-break space \u00A0, the BOM \uFEFF, and every Unicode Space_Separator character (em-space, thin space, ideographic space…). An implementation that hardcodes four characters fails on '\u00A0hello\u00A0' — which is exactly the string an interviewer will paste, because   shows up constantly in scraped HTML.
  2. The obvious regex is a performance trap — see below.

Two-pointer implementation (the safe answer)

// The spec's WhiteSpace + LineTerminator sets, written as \uXXXX escapes
// so every member is visible. \s matches exactly this same set — see below.
const WHITESPACE = new Set([
  " ", "\t", "\n", "\r", "\v", "\f",
  "\u00A0",           // no-break space — catches real-world HTML
  "\u1680",           // ogham space mark
  "\u2000", "\u2001", "\u2002", // en quad, em quad, en space
  "\u2003", "\u2004", "\u2005", // em space, three-per-em, four-per-em
  "\u2006", "\u2007", "\u2008", // six-per-em, figure space, punctuation space
  "\u2009", "\u200A",           // thin space, hair space
  "\u2028", "\u2029",           // line & paragraph separators
  "\u202F", "\u205F",           // narrow no-break space, math space
  "\u3000",                     // ideographic (CJK) space
  "\uFEFF",                     // byte-order mark
]);
 
function trim(str) {
  let start = 0;
  let end = str.length - 1;
 
  while (start <= end && WHITESPACE.has(str[start])) start++;
  while (end >= start && WHITESPACE.has(str[end])) end--;
 
  return str.slice(start, end + 1);
}

Two pointers, one pass over the trimmed regions, O(n) time, no backtracking possible. In an interview you don't recite the table — you write the six ASCII entries, add \u00A0 and \uFEFF, and say "the full set is Unicode Space_Separator plus the BOM; in real code I'd lean on \s, which matches exactly this set."

trim("   hello   ");            // "hello"
trim("\u00A0\thi\n\u00A0");         // "hi"  — the nbsp case the 4-char version fails
trim("no-op");                  // "no-op"
trim(" \n\t ");                 // ""    — all whitespace
trim("");                       // ""

The regex version — and the pitfall

The one-liner everyone reaches for:

const trim = (str) => str.replace(/^\s+|\s+$/g, "");

Correct output — but \s+$ is a known catastrophic-adjacent pattern: on a long string with a huge run of interior whitespace before a non-whitespace tail ("a" + " ".repeat(50000) + "b"), the $-anchored alternative forces the engine to attempt a whitespace match at every one of those 50,000 positions and fail at each. It degrades to O(n²) — this exact shape caused Cloudflare's 2019 global outage and is a recurring source of ReDoS advisories in npm trim-adjacent packages. The fixes, in order of preference:

str.replace(/^\s+/, "").replace(/\s+$/, "");   // still O(n²) worst case on the tail
str.replace(/^\s*(.*?)\s*$/s, "$1");           // lazy middle — better, still regex-subtle
// or: don't use a regex for the tail — scan backwards (the two-pointer version)

The interview point isn't memorizing which variant is safe — it's knowing that anchored \s+$ on untrusted input is a DoS-shaped pattern and being able to offer the linear-scan alternative. That single sentence is worth more than the implementation.

Edge cases interviewers probe

  • \u00A0 (nbsp) — pasted from any web page; the four-character version silently fails.
  • All-whitespace and empty strings — pointer crossover must yield "", not a crash or the original string.
  • \u200B zero-width space is NOT whitespace\s doesn't match it, native trim doesn't strip it. If your mental model is "invisible characters," you'll over-trim. A fantastic gotcha to name unprompted.
  • trimStart / trimEnd — your two loops are literally these two functions; factoring them out is a natural refactor the interviewer may request.
  • Immutability — strings are immutable; you return a new string. And slice(start, end + 1) — the off-by-one on end is the most common silent bug in the two-pointer version.

Common mistakes

  • Hardcoding ' ', '\t', '\n', '\r' and calling it done.
  • str.split("").filter(...) approaches that remove interior whitespace too.
  • slice(start, end) instead of end + 1.
  • Writing /^\s+|\s+$/g and having nothing to say when asked "any performance concerns with that regex?" — that question is only ever asked when the answer is yes.
  • charCodeAt(i) <= 32 loops — strips ASCII control characters that aren't whitespace, and misses everything above \u007F.

Follow-up questions

  • "Implement trimStart only, then build trim from the pieces."
  • "What exactly does \s match in JavaScript?" — the spec WhiteSpace set plus LineTerminators — the same set native trim uses; that equivalence is why the regex version is correct (just not always fast).
  • "How would you strip zero-width characters too?" — extend the set with \u200B\u200D; note you're now beyond trim semantics and should name the function accordingly (stripInvisible).
  • "Why is the two-pointer version O(n)?" — each pointer moves monotonically; total character examinations never exceed n.
  • "Where does this pattern reappear?" — hand-rolled scanning with an index cursor is the core move of the string tokenizer and JSON.parse questions; trim is the smallest member of that family.

  • String Tokenizer

    intermediate

    Split source text into tokens — a warm-up for parsing questions like JSON.parse.

  • Implement reduce including the no-initial-value case and empty-array TypeError.

  • JSON.parse

    advanced

    Write a small recursive-descent parser — the deepest polyfill question in the set.

  • Serialize values by hand: undefined, functions, cycles, and all the special cases JSON defines.