How to Modify a String in JavaScript (Without Fighting Immutability)

I still see teams lose time to tiny string edits that become messy because they forget one core fact: JavaScript strings are immutable. You can read a character by index, but you can’t change it in place. If you try, the original string stays the same and your code quietly fails. That’s why I treat every string edit as a construction problem: build a new string from old parts. Once you adopt that mindset, the tools feel predictable and you can choose the method that matches the job.

You’ll see me start with the basics of representing strings, then move into the three workhorse approaches I actually use: replace for targeted changes, slice/substring for precise insertion and deletion, and array-style edits when I need many small changes. I’ll also show you how I structure edits in real code, the mistakes I still catch in code reviews, and the performance ranges you should expect in modern engines. If you’re shipping UI, writing CLI tools, or cleaning up user input, these patterns will save you from subtle bugs and make your edits readable and testable.

Strings are immutable, and that changes your mindset

JavaScript strings store text as sequences of UTF-16 code units. You can read them like arrays, but you can’t change them in place. This is why text[2] = ‘x‘ doesn’t do what your brain expects. Instead, any edit must create a new string. Once you accept that, the rest of the tooling makes sense.

I still start by sanity-checking how a string is represented so my examples are clear and runnable. You can create strings with single or double quotes, embed quotes by alternating them, and use escapes for special characters. You can also create string objects via new String, though I avoid that in production because it adds wrapper semantics and can break strict comparisons.

// Basic forms

const greetingA = ‘Hello‘;

const greetingB = "Hello"; // valid, but I stick to single quotes

// Quotes inside a string by alternating

const quoteA = ‘She said "ship it" today‘;

const quoteB = "He replied ‘done‘";

// Escapes for special characters

const escaped = ‘Line 1\nLine 2\tTabbed‘;

// String object (rarely useful in app code)

const boxed = new String(‘Boxed‘);

When I teach this, I use a simple analogy: a string is a printed receipt. You can read it, highlight parts, or copy it, but you can’t erase a single letter from the paper itself. If you want a new version, you print a new receipt with the edits. That’s the mental model I carry into every edit.

Replace: the everyday tool, plus patterns and callbacks

For simple edits, I reach for replace. It is clear, readable, and usually fast enough. The key is remembering that it returns a new string and does not change the original.

const message = ‘Ava shipped the report‘;

const updated = message.replace(‘report‘, ‘summary‘);

console.log(message); // Ava shipped the report

console.log(updated); // Ava shipped the summary

I use the regex form when I need multiple matches or more control. You can pair it with flags like g for global matches and i for case-insensitive matches. You can also provide a callback when the replacement depends on each match.

const raw = ‘ticket-41, ticket-42, ticket-99‘;

const normalized = raw.replace(/ticket-(\d+)/g, (match, id) => {

// Pad ids to 4 digits

return T-${id.padStart(4, ‘0‘)};

});

console.log(normalized); // T-0041, T-0042, T-0099

I recommend replaceAll if you truly want every exact substring replaced and you are not using regex. It reads better than a regex with g and avoids accidental pattern behavior.

const note = ‘Ready. Ready. Ready.‘;

const once = note.replace(‘Ready‘, ‘Set‘);

const all = note.replaceAll(‘Ready‘, ‘Set‘);

console.log(once); // Set. Ready. Ready.

console.log(all); // Set. Set. Set.

When I’m editing user input, I also plan for edge cases: empty strings, missing matches, and unexpected casing. A simple way to handle case-insensitive replacement without a regex is to normalize casing first, but that can change real text. If the casing matters, I prefer a regex with the i flag and a callback that keeps the original match where needed.

Slice and substring: precise insertion and deletion

When I need to insert or remove text at a known index, I build the new string using slice or substring. This is the cleanest approach for deterministic edits and avoids the pitfalls of regex.

const name = ‘Ava‘;

const index = 1;

// Insert a character at index 1

const updated = name.slice(0, index) + ‘r‘ + name.slice(index);

console.log(updated); // Arva

I prefer slice for most work because it handles negative indices in a predictable way and matches array behavior. substring swaps its arguments if you provide them out of order, which is sometimes nice but often hides mistakes. Pick one and be consistent.

Here’s a realistic example: inserting a dash into a product key at a known position.

const rawKey = ‘ZX9QW3‘;

const dashAt = 3;

const formattedKey = rawKey.slice(0, dashAt) + ‘-‘ + rawKey.slice(dashAt);

console.log(formattedKey); // ZX9-QW3

For deletion, I use the same pattern, but I skip the chunk I want to remove.

const url = ‘https://api.example.com/v1‘;

const withoutProtocol = url.slice(‘https://‘.length);

console.log(withoutProtocol); // api.example.com/v1

If you are editing by index and you have surrogate pairs or emoji, you need to remember that slice works on UTF-16 code units. That means a single emoji may be counted as two units. For user-facing text, I usually convert to an array of code points first (more on that below) and then reassemble.

Character arrays: split, spread, Array.from, Object.assign

When I need to change multiple characters or run a series of edits, I move to an array of characters. That gives me the feel of mutable editing while still producing a new string at the end.

There are several ways to convert to a character array. Each has slightly different behavior with surrogate pairs, so choose with intent.

const label = ‘NodeJS‘;

// split

const a1 = label.split(‘‘);

a1[0] = ‘C‘;

const s1 = a1.join(‘‘);

// spread

const a2 = [...label];

a2[4] = ‘4‘;

const s2 = a2.join(‘‘);

// Array.from

const a3 = Array.from(label);

a3[1] = ‘-‘;

const s3 = a3.join(‘‘);

// Object.assign

const a4 = Object.assign([], label);

a4[2] = ‘_‘;

const s4 = a4.join(‘‘);

console.log(s1, s2, s3, s4);

In practice, I default to Array.from or spread because they also work correctly with many Unicode code points, while split(‘‘) can split surrogate pairs into broken pieces. Here’s a quick example with emoji:

const flag = ‘🏳️‍🌈‘;

console.log(flag.length); // length in UTF-16 code units

console.log([...flag].length); // length in code points

console.log(flag.split(‘‘).length); // can be larger than expected

When I need to modify by character positions that a user can see, I go with Array.from or spread. When I’m editing a known ASCII-only token, split(‘‘) is fine and a bit faster.

Structured edits at scale: pipelines and helper functions

As soon as I see three or more edits in one function, I extract helpers. This keeps the intent clear and makes the code testable. I also favor small, single-purpose helpers that accept and return strings so they are easy to compose.

Here’s a pattern I use for cleaning up a filename and inserting a timestamp:

const stripInvalid = (text) => text.replace(/[:"/\\|?*]/g, ‘-‘);

const collapseSpaces = (text) => text.replace(/\s+/g, ‘ ‘).trim();

const appendTimestamp = (text, date = new Date()) => {

const stamp = date.toISOString().slice(0, 10);

return ${text}-${stamp};

};

const raw = ‘ 2026 Report: Q1/Q2 ‘;

const cleaned = appendTimestamp(collapseSpaces(stripInvalid(raw)));

console.log(cleaned); // 2026 Report- Q1-Q2-2026-01-09

This style also plays well with 2026 tooling. I’ll often add quick unit tests and use AI-assisted test generation to cover edge cases like empty input, already-clean input, and tricky Unicode. But I still review the generated tests because string functions are easy to mis-specify.

If you prefer a more data-driven approach, I sometimes build a small edit pipeline with explicit steps:

const edits = [

(s) => s.replace(/\u00A0/g, ‘ ‘), // non-breaking space

(s) => s.replace(/\s+/g, ‘ ‘).trim(),

(s) => s.replace(/[“”]/g, ‘"‘),

];

const applyEdits = (text) => edits.reduce((acc, fn) => fn(acc), text);

console.log(applyEdits(‘“Ava” says hi‘));

That pipeline approach makes it clear where each change happens and encourages you to add or remove steps without fear.

Traditional vs modern editing styles (and when I pick each)

I still see legacy code that does manual index math everywhere, and I see newer code that chains high-level APIs. Both can be valid, but I choose based on the clarity of intent and the likelihood of change.

Style

Typical approach

When I use it

When I avoid it

Traditional

index math with slice and substring

fixed-format strings like IDs or timestamps

user text with emoji or unknown length

Modern

replace, regex callbacks, code-point arrays

user text, logs, cleanup tasks

ultra-hot loops where GC pressure is a concernIf I can express the edit in a single replace or replaceAll, I do that. If I need precise control of indices, I use slice. If the text can include emoji or combining marks, I choose a code-point array and rebuild. The goal is a future reader knowing exactly what the change is supposed to do.

Performance notes and memory tradeoffs

I rarely choose string methods based on speed alone, but I do keep a sense of scale. For small to medium strings, replace and slice are usually fast enough that the differences are not visible to users. On larger inputs, allocation and garbage collection can become the dominant cost.

A few practical ranges I use when reasoning about performance in modern engines:

  • For small strings under a few kilobytes, differences between replace and slice are typically not measurable outside a benchmark harness.
  • For larger inputs (hundreds of kilobytes or more), chained replacements can start to show visible pauses, often in the 10–25ms range on a typical laptop.
  • Converting to an array and back adds overhead, but it becomes worthwhile if you perform many small edits, because you avoid repeated full-string allocations.

If you are processing very large text streams, consider chunking your input and editing in pieces rather than building and rebuilding one giant string. I also recommend keeping intermediate allocations to a minimum by using a single pass where possible. That can mean a regex with a callback, or a loop that builds an array of characters and joins once.

Common mistakes and guardrails I use in reviews

I see the same bugs over and over. Here are the ones I flag most often, plus the guardrails I recommend:

1) Assuming replace changes the original string.

const title = ‘Weekly Report‘;

title.replace(‘Weekly‘, ‘Monthly‘);

console.log(title); // still ‘Weekly Report‘

Guardrail: assign the result or return it from a helper.

2) Forgetting replace only changes the first match.

const text = ‘red red red‘;

console.log(text.replace(‘red‘, ‘blue‘)); // blue red red

Guardrail: use replaceAll or a regex with g when you truly need every match.

3) Breaking emoji with split(‘‘).

const msg = ‘Hi 👋🏽‘;

console.log(msg.split(‘‘)); // fragments the emoji

Guardrail: use Array.from or spread for user-facing text.

4) Indexing errors when mixing substring and slice.

Guardrail: choose one and stick to it. I use slice by default.

5) Mixing string objects and string primitives.

Guardrail: avoid new String in app code unless you have a strong reason.

When I do not modify strings directly

Sometimes the right answer is not a string edit at all. Here are cases where I choose a different route:

  • Structured data like JSON, URLs, or file paths: I parse and update fields rather than manual string edits.
  • User-visible localization strings: I edit source keys and let the translation system handle rendering.
  • Repeated edits to a large text blob: I use a tokenization step or a builder that assembles the output once.

That doesn’t mean you avoid string edits entirely; it means you keep them where they are clear and safe. A clean string edit is easy to reason about. A fragile one looks like it works until it breaks on edge cases.

New section: A quick map of the string methods I reach for

Before I go deeper, I like to keep a mental map of the methods I actually use so I’m not guessing in the moment. This is the short list that shows up in my day-to-day work:

  • replace and replaceAll for search-and-replace edits.
  • slice for deterministic insertions, deletions, and extractions.
  • trim, trimStart, trimEnd for whitespace cleanup.
  • padStart, padEnd for fixed-width formatting.
  • startsWith, endsWith, and includes for guard clauses and quick checks.
  • split (or Array.from / spread) plus join for multi-step character edits.

I don’t use everything in the standard library. I use a small, consistent set so future changes are easier to read. If I find myself using a method I don’t recognize, I stop and ask: could I express this more clearly with a method I already know well?

New section: Basic edits you can memorize

These are the edits I teach junior teammates first. They are small, but they show the “build a new string” pattern cleanly.

Replace a word

const sentence = ‘Ship the package‘;

const fixed = sentence.replace(‘package‘, ‘parcel‘);

Insert at a position

const sku = ‘AB12CD‘;

const withDash = sku.slice(0, 2) + ‘-‘ + sku.slice(2);

Delete a range

const id = ‘user:12345‘;

const withoutPrefix = id.slice(‘user:‘.length);

Add a prefix or suffix safely

const filename = ‘report.pdf‘;

const stamped = 2026-${filename}; // simple template

Normalize whitespace

const messy = ‘  Hello    world  ‘;

const clean = messy.replace(/\s+/g, ‘ ‘).trim();

I reach for these even in complex pipelines because they are easy to read and easy to test.

New section: Insertion, deletion, and replacement by index

When the business logic talks about “position 5” or “character 10,” I avoid regex and do explicit slicing. It’s honest about what’s happening and avoids the hidden behavior of pattern matching.

Replace a character at a known index

const code = ‘A1C3‘;

const index = 2; // zero-based

const updated = code.slice(0, index) + ‘B‘ + code.slice(index + 1);

Insert a substring

const base = ‘helloWorld‘;

const insertAt = 5;

const spaced = base.slice(0, insertAt) + ‘ ‘ + base.slice(insertAt);

Remove a substring by range

const text = ‘2026-01-09T12:00Z‘;

const withoutTime = text.slice(0, 10);

You’ll notice I do the index math explicitly. That’s a feature, not a bug. When code reviewers read this, they can verify the intent quickly.

New section: Editing user-visible text (Unicode reality check)

If you only work with ASCII, string editing is simple. But user-visible text includes emojis, accents, and combined characters. That’s where mistakes creep in.

In JavaScript, strings are sequences of UTF-16 code units, not human-perceived characters. A single emoji like “👋🏽” can be multiple code points, and a single visible letter like “é” might be represented as “e” plus a combining accent.

That means an index that looks correct in a visual sense can be wrong at the code-unit level. Here’s how I handle it:

1) If the string is machine-generated and ASCII-only, I use slice and replace freely.

2) If the string is user-facing, I use code-point aware methods like spread or Array.from.

3) If the string contains complex grapheme clusters (like emoji sequences or accented characters), I avoid “edit by index” and instead do semantic replacements based on a match or a known token.

A practical example: capitalize the first user-visible character safely.

const capitalizeFirst = (text) => {

const chars = Array.from(text);

if (chars.length === 0) return text;

chars[0] = chars[0].toUpperCase();

return chars.join(‘‘);

};

console.log(capitalizeFirst(‘éclair‘)); // Éclair

console.log(capitalizeFirst(‘👋🏽 hello‘)); // 👋🏽 hello (emoji stays intact)

If you need more precise grapheme handling (like “what a user sees as one character”), you can use Intl.Segmenter in modern environments. I don’t reach for it often, but when I do, it usually saves me from subtle bugs.

const segmenter = new Intl.Segmenter(‘en‘, { granularity: ‘grapheme‘ });

const toGraphemes = (text) => Array.from(segmenter.segment(text), s => s.segment);

const graphemes = toGraphemes(‘👨‍👩‍👧‍👦 family‘);

console.log(graphemes[0]); // whole family emoji

New section: Regex replacement patterns I actually trust

Regex is powerful, but it’s easy to overuse. I keep a small set of patterns that solve common problems without becoming unreadable.

Strip all non-digit characters

const phone = ‘(415) 555-0199‘;

const digits = phone.replace(/\D/g, ‘‘);

Normalize multiple spaces and tabs

const messy = ‘A\t\tB   C‘;

const clean = messy.replace(/\s+/g, ‘ ‘);

Keep casing but normalize separators

const slugify = (text) =>

text.trim()

.replace(/\s+/g, ‘-‘)

.replace(/[^a-zA-Z0-9\-]/g, ‘‘);

Use a callback to compute a value

const text = ‘Item 4 costs 7 dollars‘;

const doubled = text.replace(/\d+/g, (m) => String(Number(m) * 2));

// Item 8 costs 14 dollars

My rule: if a regex doesn’t fit on one line or isn’t obvious, I add a comment or extract it into a named constant. The regex itself is a tiny program, and future readers deserve to know what it does.

New section: Working with substrings safely

String edits are often surrounded by conditional logic. I use guard checks to avoid unnecessary allocations and to keep behavior explicit.

Guarding with includes or startsWith

const addProtocol = (url) => {

if (url.startsWith(‘http://‘) || url.startsWith(‘https://‘)) return url;

return https://${url};

};

Avoiding edits when nothing changes

const normalizeNewlines = (text) => {

if (!text.includes(‘\r\n‘)) return text;

return text.replace(/\r\n/g, ‘\n‘);

};

These small checks can avoid work in hot code paths and make behavior easier to reason about in tests.

New section: Practical scenarios and how I solve them

These are scenarios I’ve seen in real projects. The goal is to show how the same concepts apply outside a toy example.

Scenario 1: Masking a credit card number

I want to show only the last 4 digits. I avoid regex here and use slice for clarity.

const maskCard = (number) => {

const digits = number.replace(/\D/g, ‘‘);

const last4 = digits.slice(-4);

return last4.padStart(digits.length, ‘•‘);

};

console.log(maskCard(‘4111 1111 1111 1234‘)); // ••••••••••••1234

Scenario 2: Normalizing a username input

I want to trim whitespace, lowercase it, and replace spaces with underscores.

const normalizeUser = (name) =>

name.trim().toLowerCase().replace(/\s+/g, ‘_‘);

console.log(normalizeUser(‘ Ava Q. Smith ‘)); // avaq.smith

Scenario 3: Converting CSV headers to camelCase

This uses a callback to transform each word after the first.

const toCamel = (text) =>

text

.trim()

.toLowerCase()

.replace(/[\s-]+(.)/g, (, ch) => ch.toUpperCase());

console.log(toCamel(‘Order Total Amount‘)); // orderTotalAmount

Scenario 4: Inserting a checksum into an ID

This is a pure index insertion example.

const insertChecksum = (id, checksum) => {

const at = 4;

return id.slice(0, at) + checksum + id.slice(at);

};

Scenario 5: Updating query parameters safely

If the data is structured, I don’t do raw string edits. I parse and rebuild.

const addParam = (url, key, value) => {

const u = new URL(url);

u.searchParams.set(key, value);

return u.toString();

};

That last one is a good example of “don’t edit strings directly.” I do it this way to avoid broken URLs.

New section: Mutation mindset for string editing

I often see code that tries to “edit” strings the way you’d edit an array. If you think in terms of mutation, you end up with confusing logic. If you think in terms of construction, the code becomes simpler.

Here’s how I mentally translate it:

  • “Change the third character to X” becomes “take the prefix, add X, then add the suffix.”
  • “Remove the word after the colon” becomes “split, drop, join.”
  • “Replace all instances of a token” becomes “use replaceAll and return a new string.”

This mindset prevents subtle bugs. It also nudges you to write small helpers and to return strings rather than modifying global variables.

New section: Testing string edits the way I actually do it

String edits are deceptively easy to break. I test them with a small matrix of edge cases. My pattern is simple:

1) The happy path.

2) Empty input.

3) Input that already satisfies the rule.

4) Input that contains weird Unicode.

5) Input that contains multiple occurrences when I expect one.

Here’s a minimal example for a “slugify” helper:

const slugify = (text) =>

text

.trim()

.toLowerCase()

.replace(/\s+/g, ‘-‘)

.replace(/[^a-z0-9-]/g, ‘‘);

console.log(slugify(‘Hello World‘)); // hello-world

console.log(slugify(‘ spaced out ‘)); // spaced-out

console.log(slugify(‘Already-slugged‘)); // already-slugged

console.log(slugify(‘Emoji 👋🏽 Test‘)); // emoji-test

console.log(slugify(‘‘)); // ‘‘

In a real codebase, I’d turn these into unit tests. The effort is tiny, but it saves you from regressions when you inevitably tweak the rules later.

New section: When array-style editing wins

Array-style editing can feel heavy, but it’s perfect for multiple small changes that would otherwise require many full-string allocations. This is a good fit for rules like “change these 12 characters at specific positions.”

Here’s a practical example: sanitize a serial number by replacing all letters with X but preserving digits and dashes.

const sanitizeSerial = (text) => {

const chars = Array.from(text);

for (let i = 0; i < chars.length; i += 1) {

const c = chars[i];

if (/[A-Za-z]/.test(c)) chars[i] = ‘X‘;

}

return chars.join(‘‘);

};

console.log(sanitizeSerial(‘AZ-19-BQ‘)); // XX-19-XX

You could do this with regex, but the array loop makes the intent explicit, and it’s easier to extend with additional logic.

New section: Working with very large text

If you process large logs or big text blobs, I shift the approach slightly:

  • I avoid repeated replace calls in a long chain.
  • I prefer a single pass with a regex callback or a loop that appends to an array.
  • If the input is huge, I process it in chunks.

Here’s a chunking pattern I use when I need to normalize a big file:

const normalizeChunks = (chunks) => {

const result = [];

for (const chunk of chunks) {

// Each chunk is a string

const cleaned = chunk.replace(/\r\n/g, ‘\n‘);

result.push(cleaned);

}

return result.join(‘‘);

};

This keeps memory usage predictable and avoids building a massive intermediate string repeatedly.

New section: Minimal string-editing utilities I reuse

Over time I’ve collected a few helpers that solve 80% of my use cases. They’re tiny, but they reduce errors.

const replaceAllSafe = (text, search, replacement) =>

text.split(search).join(replacement); // works for plain strings

const ensurePrefix = (text, prefix) =>

text.startsWith(prefix) ? text : prefix + text;

const ensureSuffix = (text, suffix) =>

text.endsWith(suffix) ? text : text + suffix;

const removePrefix = (text, prefix) =>

text.startsWith(prefix) ? text.slice(prefix.length) : text;

const removeSuffix = (text, suffix) =>

text.endsWith(suffix) ? text.slice(0, -suffix.length) : text;

Notice I’m explicit about when I use string-based replaceAll or the split/join pattern. The split/join method is great if I want exact substring replacement without any regex behavior.

New section: Comparing slice, substring, and substr

I keep this comparison short because I don’t want to encourage use of the old substr method. It’s legacy and not recommended.

  • slice(start, end) handles negative indices and is consistent with arrays.
  • substring(start, end) swaps arguments when start > end, which can hide mistakes.
  • substr(start, length) is legacy and should be avoided in new code.

If you’ve got a mixed codebase, pick one (I pick slice) and standardize on it. The team will thank you later.

New section: Subtle bugs I’ve fixed in real projects

I’ll share a few examples I’ve seen in the wild and how I correct them.

Bug: Off-by-one in index replacements

// Incorrect: removes the wrong character

const fix = (text) => text.slice(0, 2) + text.slice(4);

This removes two characters instead of one. I fix it by being explicit about the range.

const fix = (text) => text.slice(0, 2) + text.slice(3);

Bug: Accidental regex when a plain string was intended

// ‘.‘ is regex for any character

text.replaceAll(‘.‘, ‘_‘);

This replaces every character, not just dots. If you want literal dots, pass a string:

text.replaceAll(‘.‘, ‘_‘); // ok because search is a string, not a regex

The confusion usually comes from mixing regex and string arguments. I make that distinction obvious in my code reviews.

Bug: Editing the wrong layer of data

People try to update JSON by string editing instead of parsing. This breaks when spacing changes.

const updated = jsonString.replace(‘"status":"old"‘, ‘"status":"new"‘);

I fix it by parsing and re-stringifying.

const obj = JSON.parse(jsonString);

obj.status = ‘new‘;

const updated = JSON.stringify(obj);

New section: A practical decision tree I use

When I’m not sure what method to use, I run through this quick decision tree:

1) Is the data structured (JSON, URL, file path)? If yes, parse it instead of editing raw strings.

2) Is this a simple, single replacement? If yes, use replace or replaceAll.

3) Is this a precise index edit? If yes, use slice with explicit index math.

4) Are there multiple small edits? If yes, convert to an array, mutate, and join once.

5) Is the text user-visible and Unicode-heavy? If yes, use code-point arrays or Intl.Segmenter.

This is the difference between a 2-line change and a bug that takes an afternoon to diagnose.

New section: Style consistency and readability

The best string edit is the one that reads like a sentence. A few habits help:

  • Use descriptive variable names like prefix, suffix, insertAt.
  • Avoid nesting too many string operations in one line unless it’s a simple pipeline.
  • Keep regex simple or pull it into a named constant with a comment.
  • Prefer templates or explicit concatenation when it improves clarity.

Here’s a version of a messy edit rewritten for clarity:

// Messy

const out = s.replace(/\s+/g,‘ ‘).replace(/\.$/,‘‘).trim();

// Clear

const collapseSpaces = (t) => t.replace(/\s+/g, ‘ ‘);

const removeTrailingPeriod = (t) => t.replace(/\.$/, ‘‘);

const out = removeTrailingPeriod(collapseSpaces(s)).trim();

I write the clear version by default unless I’m working in a hot path where the function call overhead is proven to be a problem.

Closing thoughts and next steps

If you remember only one idea, remember this: a string edit in JavaScript always creates a new string. That single fact explains why replace returns a value, why index assignment does nothing, and why slice-based edits are so common. Once you build on that, you can pick the method that keeps your intent obvious to the next person reading your code.

Here’s how I choose in practice: for a single change, I use replace or replaceAll. For precise edits by index, I use slice because it reads cleanly and behaves consistently. For many small edits, I convert to a character array, change what I need, and join once. When text contains emoji or other Unicode code points, I use spread or Array.from so I don’t break the visible characters. And when the data is structured, I edit the structure instead of the string.

If you want to get better at this quickly, pick a small utility function in your codebase and rewrite it using these patterns. Add tests for empty strings, missing matches, and emoji. That practice will pay off immediately because string edits tend to be scattered across UI, API, and CLI code. The more consistent you are, the fewer surprises you’ll see in production.

Scroll to Top