DEV Community

Cover image for Strings look simple... until they surprise you.
Shajibul Alam Shihab
Shajibul Alam Shihab

Posted on

Strings look simple... until they surprise you.

Strings & Text Processing: Text Isn't as Simple as It Looks

If someone asks you what's the simplest type of data in programming, there's a good chance you'll say strings.

After all, they're just text.

A person's name is a string.

An email address is a string.

A password is a string.

Even the message you're reading right now is just a collection of strings.

At first glance, there doesn't seem to be much to learn. You create a string, print it, compare it with another string, and move on.

But after writing a few programs, things start getting... strange.

You convert "hello" into uppercase, yet the original string somehow stays the same.

An emoji that looks like a single character suddenly reports a length of 2.

Joining thousands of small strings together makes your program unexpectedly slower.

And then someone introduces you to something called Regex, which looks less like code and more like an ancient spell.

None of these are bugs. They're simply the result of how strings actually work behind the scenes.

In this lesson, we'll uncover those hidden details one by one.


Surprise #1: Why Didn't the String Change?

Take a look at this code.

const original = "hello";
const shouted = original.toUpperCase();

console.log(original);
console.log(shouted);
Enter fullscreen mode Exit fullscreen mode

What would you expect the output to be?

Many beginners think the output will look like this:

HELLO
HELLO
Enter fullscreen mode Exit fullscreen mode

But that's not what happens.

Instead, JavaScript prints:

hello
HELLO
Enter fullscreen mode Exit fullscreen mode

The original string remains exactly as it was.

Why?

Because strings are immutable.


What Does "Immutable" Mean?

The word immutable simply means:

Once something is created, it cannot be changed.

Think of a printed book.

If you want every occurrence of the word hello to become HELLO, you don't magically change the ink that's already on the paper.

Instead, you print a new copy with the updated text.

Strings behave in a similar way.

Whenever you use methods like:

  • toUpperCase()
  • toLowerCase()
  • replace()
  • concat()

the original string stays untouched.

Instead, JavaScript creates a brand-new string and returns it.

That's why this works:

const original = "hello";
const shouted = original.toUpperCase();
Enter fullscreen mode Exit fullscreen mode

Instead of modifying original, JavaScript creates another string containing "HELLO" and stores it inside shouted.

This behavior isn't unique to JavaScript.

Python, Java, C#, and many modern programming languages treat strings the same way.


Why Design Strings This Way?

At first, immutability sounds like an unnecessary restriction.

Why not simply change the existing string?

Because immutable data is much safer.

Imagine two different parts of your program are using the same string.

If one part secretly changes it, the other part suddenly starts seeing different data, which can lead to bugs that are extremely difficult to track down.

By making strings immutable, programming languages guarantee that once a string is created, nobody can unexpectedly modify it.

Whenever a change is needed, a completely new string is created instead.

It might seem like a small design decision, but it prevents countless bugs in large applications.


A Hidden Cost of Creating New Strings

Creating new strings is usually so fast that you never notice it.

But imagine doing it thousands or even millions of times.

Consider this loop:

let result = "";

for (const word of ["JavaScript", "is", "awesome"]) {
    result += word;
}
Enter fullscreen mode Exit fullscreen mode

The code looks perfectly reasonable.

But here's what's happening behind the scenes.

Initially:

""
Enter fullscreen mode Exit fullscreen mode

After the first iteration:

"JavaScript"
Enter fullscreen mode Exit fullscreen mode

The old empty string is discarded.

Next iteration:

"JavaScriptis"
Enter fullscreen mode Exit fullscreen mode

The previous string is discarded again.

Then:

"JavaScriptisawesome"
Enter fullscreen mode Exit fullscreen mode

Another new string is created.

Even though we only care about the final result, JavaScript had to create multiple temporary strings along the way.

For three words, that's not a problem.

For three million words, it becomes expensive.


The Better Approach

When building large pieces of text, programmers usually avoid repeatedly using +=.

Instead, they collect all the pieces first.

const parts = [];

for (const word of ["JavaScript", "is", "awesome"]) {
    parts.push(word);
}

const result = parts.join("");
Enter fullscreen mode Exit fullscreen mode

Instead of constantly creating new strings, we simply store each word inside an array.

Only at the very end do we combine everything into one final string.

This creates far fewer temporary objects and is generally much more efficient when working with large amounts of text.

That doesn't mean you should never use +=.

For small strings, it's perfectly fine and often easier to read.

But when performance matters or when you're building very large strings inside loops collecting pieces first and joining them later is the better approach.


Strings Seem Simple... Until They Aren't

So far, we've learned two important things.

First, strings never change after they're created.

Second, every "modification" actually creates a completely new string.

Most of the time, that's all you need to know.

But then something even stranger happens.

You write this:

"😀".length
Enter fullscreen mode Exit fullscreen mode

And JavaScript replies:

2
Enter fullscreen mode Exit fullscreen mode

Not 1.

2.

How can a single emoji have a length of two?

To answer that, we need to look beyond strings themselves and understand how computers store text in memory.


Surprise #2: Why Does One Emoji Have a Length of 2?

At first, this feels like a bug.

You can clearly see one emoji.

"😀".length
Enter fullscreen mode Exit fullscreen mode

Yet JavaScript says:

2
Enter fullscreen mode Exit fullscreen mode

How can one visible character suddenly become two?

The answer has nothing to do with emojis.

It has everything to do with how computers store text.


Computers Don't Understand Letters

When you type the letter:

A
Enter fullscreen mode Exit fullscreen mode

you see a character.

But your computer doesn't.

Computers only understand numbers.

So before any text can be stored, every character must first be converted into numbers.

This conversion is called text encoding.

Think of encoding as a giant dictionary.

Instead of storing the actual letter A, the computer stores a number that represents A.

When it reads that number again, it knows to display the letter A on your screen.

The same idea works for every language.

Whether it's English,

Hello
Enter fullscreen mode Exit fullscreen mode

Arabic,

مرحبا
Enter fullscreen mode Exit fullscreen mode

Japanese,

こんにちは
Enter fullscreen mode Exit fullscreen mode

or Bengali,

হ্যালো
Enter fullscreen mode Exit fullscreen mode

they're all stored as numbers behind the scenes.


Enter UTF-8

Years ago, computers mostly dealt with English text.

A simple encoding called ASCII was enough because it only needed to represent English letters, digits, and a few symbols.

But the internet changed everything.

Suddenly, computers needed to store text from almost every language in the world.

Not just that they also needed to support symbols, currency signs, mathematical operators, and eventually emojis.

That's why modern software largely uses an encoding called UTF-8.

UTF-8 is clever.

Instead of giving every character the same amount of storage, it uses only as much space as needed.

For example:

  • Most English letters use 1 byte.
  • Many accented characters use 2 bytes.
  • Some Asian characters use 3 bytes.
  • Many emojis use 4 bytes.

This keeps English text compact while still allowing virtually every written language to be represented.


Then Why Does JavaScript Say 2?

Here's the confusing part.

Even though UTF-8 is the most common encoding for storing and transmitting text, JavaScript doesn't use UTF-8 to calculate .length.

Instead, JavaScript stores strings internally using UTF-16.

And .length doesn't count "characters" the way humans see them.

It counts UTF-16 code units.

Most everyday characters fit into a single code unit.

"A".length
// 1

"B".length
// 1

"7".length
// 1
Enter fullscreen mode Exit fullscreen mode

But many emojis are too large to fit into just one UTF-16 code unit.

So JavaScript stores them using two code units.

That's why this happens:

"😀".length
Enter fullscreen mode Exit fullscreen mode

Output:

2
Enter fullscreen mode Exit fullscreen mode

You still see one emoji.

But internally, JavaScript needs two UTF-16 code units to represent it.

So .length reports 2.


Wait... Does length Lie?

Not exactly.

It simply answers a different question.

Most beginners assume .length means:

"How many characters can I see?"

But JavaScript is actually answering:

"How many UTF-16 code units are stored?"

Those aren't always the same thing.

For everyday English text, they usually match.

For emojis and some less common Unicode characters, they don't.

That's why it's safer to think of .length as:

"The internal length of this JavaScript string."

rather than

"The number of visible characters."


This Doesn't Only Affect Emojis

Emojis are the easiest example because they're familiar.

But they're not the only ones.

Some languages contain characters that are formed by combining multiple Unicode values.

Some accented letters can also be represented in more than one way.

Even certain flags are actually made by combining two separate Unicode characters into what looks like a single symbol.

For example:

🇺🇸
Enter fullscreen mode Exit fullscreen mode

looks like one flag.

Behind the scenes, it's actually composed of multiple Unicode code points.

This is why working with text across different languages can be surprisingly complex.

Fortunately, for most everyday applications, you don't need to worry about these details unless you're doing advanced text processing.

Still, it's useful to know that what humans see as one character isn't always what the computer stores as one unit.


Now That We Know How Text Is Stored...

Understanding encoding explains a lot of strange behavior.

But most of the time, we're not thinking about bytes or Unicode.

We're simply trying to answer practical questions like:

  • How do I remove extra spaces from user input?
  • How do I split a full name into first and last name?
  • How do I check whether a sentence contains a specific word?
  • How do I extract all the numbers from a block of text?

These are the kinds of operations you'll use almost every day.

Fortunately, programming languages give us a rich toolbox for working with strings.


The Everyday String Toolbox

Most of the time, we don't spend our day thinking about UTF-8, UTF-16, or immutable strings.

We're simply trying to work with text.

Maybe we're reading a user's name from a form.

Maybe we're processing a CSV file.

Maybe we're cleaning data before saving it to a database.

Fortunately, most programming languages provide a handful of string methods that you'll use over and over again.

To keep things simple, let's work with the same piece of text throughout this section.

const raw = "  Ada Lovelace, 1815-1852  ";
Enter fullscreen mode Exit fullscreen mode

At first glance, it looks fine.

But if you look carefully, you'll notice two extra spaces at the beginning and two more at the end.

Small details like these are surprisingly common when dealing with user input.

Let's clean it up.


Step 1: Remove Unwanted Spaces

The first tool every programmer should know is trim().

const trimmed = raw.trim();
Enter fullscreen mode Exit fullscreen mode

Now our string becomes:

Ada Lovelace, 1815-1852
Enter fullscreen mode Exit fullscreen mode

The extra spaces are gone.

One important thing to remember is that trim() doesn't change the original string.

Remember what we learned earlier?

Strings are immutable.

So this:

raw.trim();
Enter fullscreen mode Exit fullscreen mode

does absolutely nothing useful if you ignore the returned value.

You need to store the new string somewhere.

const trimmed = raw.trim();
Enter fullscreen mode Exit fullscreen mode

This tiny habit saves beginners from a lot of confusion.


Why Is trim() So Important?

Imagine a login form.

A user accidentally types:

john@example.com␠
Enter fullscreen mode Exit fullscreen mode

Notice the invisible space at the end.

To us, it looks exactly like:

john@example.com
Enter fullscreen mode Exit fullscreen mode

But to the computer, they're different strings.

Without trimming the input, a perfectly valid email might fail to match what's stored in your database.

That's why one of the simplest best practices is:

Whenever text comes from a user, trim it before comparing or storing it.

It removes accidental spaces without changing the meaningful part of the text.


Step 2: Break Text Into Pieces

Now that our string is clean, we can separate it into smaller parts.

Right now, everything is stored in one string.

Ada Lovelace, 1815-1852
Enter fullscreen mode Exit fullscreen mode

Suppose we want the name and the years separately.

This is exactly what split() is designed for.

const [name, years] = trimmed.split(", ");
Enter fullscreen mode Exit fullscreen mode

Now we have two independent values.

name  → Ada Lovelace

years → 1815-1852
Enter fullscreen mode Exit fullscreen mode

The text hasn't disappeared.

We've simply told JavaScript:

"Whenever you see ", ", cut the string into pieces."

This is useful for processing CSV files, parsing names, reading configuration files, and many other real-world tasks.


Step 3: Does This Text Contain Something?

Sometimes you don't need to split a string.

You just want to know whether a word exists inside it.

Suppose we have:

const sentence = "JavaScript is fun";
Enter fullscreen mode Exit fullscreen mode

If we want to know whether the word "fun" appears, we can simply write:

sentence.includes("fun");
Enter fullscreen mode Exit fullscreen mode

Result:

true
Enter fullscreen mode Exit fullscreen mode

If the word isn't present,

sentence.includes("Python");
Enter fullscreen mode Exit fullscreen mode

returns

false
Enter fullscreen mode Exit fullscreen mode

It's simple, readable, and usually the best choice when you only need a yes-or-no answer.


What About indexOf()?

Before includes() became common, developers often used indexOf().

sentence.indexOf("fun");
Enter fullscreen mode Exit fullscreen mode

Instead of returning true or false, it returns the position where the text starts.

For example:

14
Enter fullscreen mode Exit fullscreen mode

If the text isn't found, it returns:

-1
Enter fullscreen mode Exit fullscreen mode

Both methods are useful.

If you only care whether something exists, includes() is usually easier to read.

If you also need the position, indexOf() gives you that information.


Step 4: Extract Just the Part You Need

Sometimes you don't want the entire string.

You only need a small section of it.

That's where slice() comes in.

Imagine this string:

const language = "JavaScript";
Enter fullscreen mode Exit fullscreen mode

If we only want the first four letters:

language.slice(0, 4);
Enter fullscreen mode Exit fullscreen mode

we get:

Java
Enter fullscreen mode Exit fullscreen mode

The first number tells JavaScript where to start.

The second tells it where to stop.

The character at the stopping position isn't included.

You can think of slice() as cutting out a portion of the original string while leaving the original untouched.


Step 5: Build Better Strings

Sooner or later, you'll need to combine variables into a sentence.

Suppose we already have:

const name = "Ada Lovelace";
const years = "1815-1852";
Enter fullscreen mode Exit fullscreen mode

An older way to build a sentence would be:

name + " lived from " + years
Enter fullscreen mode Exit fullscreen mode

It works.

But once the sentence becomes longer, all those + operators start making the code harder to read.

Modern JavaScript gives us a cleaner alternative called template literals.

`${name} lived from ${years}`
Enter fullscreen mode Exit fullscreen mode

The result is:

Ada Lovelace lived from 1815-1852
Enter fullscreen mode Exit fullscreen mode

The variables fit naturally into the sentence, making the code much easier to write and understand.

Whenever you're building strings with variables, template literals are usually the preferred approach.


So Far, Everything Has Been Simple...

Every method we've seen works with exact pieces of text.

trim() removes spaces.

split() cuts text apart.

includes() searches for exact words.

slice() extracts a section.

Template literals build new strings.

But what happens when the text you're looking for isn't fixed?

Suppose you don't know the exact year.

You simply want to find every number inside a paragraph.

Or every email address.

Or every phone number.

Searching for exact words is no longer enough.

That's where one of the most powerful and sometimes intimidating tools in programming enters the picture:

Regular Expressions, or simply Regex.


When Simple Search Isn't Enough

So far, every string method we've used has worked with exact text.

For example:

sentence.includes("JavaScript");
Enter fullscreen mode Exit fullscreen mode

JavaScript looks for the exact word "JavaScript".

That's great when you already know what you're searching for.

But real-world text is often much messier.

Imagine someone gives you this string:

Ada Lovelace, 1815-1852
Enter fullscreen mode Exit fullscreen mode

You don't care about the name.

You don't even care about the hyphen.

You simply want every number that appears in the text.

Or imagine you're processing thousands of user comments and want to find:

  • every email address
  • every phone number
  • every URL
  • every date

You can't possibly search for every number or every email one by one.

You need a way to describe a pattern instead of an exact word.

That's exactly why Regular Expressions, commonly called Regex, exist.


Think of Regex as a Search Rule

Instead of saying,

"Find the word JavaScript."

Regex lets you say,

"Find anything that looks like a number."

or

"Find anything that looks like an email address."

or

"Find every word that starts with a capital letter."

In other words, regex doesn't search for one specific piece of text.

It searches for a pattern.

That's what makes it so powerful.


A Simple Example

Let's go back to our string:

const years = "1815-1852";
Enter fullscreen mode Exit fullscreen mode

Suppose we want to know whether this string contains any digits.

We can write:

/\d+/.test(years);
Enter fullscreen mode Exit fullscreen mode

The result is:

true
Enter fullscreen mode Exit fullscreen mode

Let's break down the pattern.

\d
Enter fullscreen mode Exit fullscreen mode

means:

Match any digit from 0 to 9.

The + means:

Match one or more of them.

So together,

\d+
Enter fullscreen mode Exit fullscreen mode

means:

Find one or more consecutive digits.

That's enough to match:

1815
Enter fullscreen mode Exit fullscreen mode

and

1852
Enter fullscreen mode Exit fullscreen mode

without us ever typing those numbers explicitly.


Extracting Every Match

Sometimes we don't just want to know whether a match exists.

We want the actual matches.

That's where match() comes in.

const allYears = years.match(/\d+/g);
Enter fullscreen mode Exit fullscreen mode

The result is:

["1815", "1852"]
Enter fullscreen mode Exit fullscreen mode

The pattern is exactly the same.

The only new thing is the g flag.

You can think of g as meaning:

"Don't stop after the first match. Keep searching until you've found them all."

Without it, JavaScript would stop after finding "1815".

With it, it continues and also finds "1852".


Regex Is Powerful...

By now, you can probably see why developers love regex.

With the right pattern, you can search through huge amounts of text and find exactly what you're looking for.

Need every number?

Regex.

Every hashtag?

Regex.

Every date?

Regex.

Every email?

Regex.

It's an incredibly versatile tool.


...But It's Also Easy to Misuse

Regex has a reputation. Not because it's bad. But because it's very easy to make it more complicated than it needs to be.

Many beginners have this experience:

They find a regex online. It seems to work. So they copy it into their project.

A month later, they look at the same line of code and think:

"I have absolutely no idea what this does."

The problem gets even worse if the regex has no comments explaining it.

A mysterious regex is like a complicated math formula with no explanation. It might be correct. But nobody wants to touch it. Including the person who originally wrote it.


Real-World Regex Is Harder Than It Looks

Take email validation as an example.

At first, it sounds easy.

Just check whether the text contains:

@
Enter fullscreen mode Exit fullscreen mode

Right?

Unfortunately, real email addresses are much more complicated.

Some contain +.

Some contain subdomains.

Some use international characters.

Some perfectly valid email addresses look surprisingly unusual.

Writing a regex that correctly accepts every valid email while rejecting every invalid one is much harder than it first appears.

That's why many developers prefer using well-tested libraries instead of writing complex validation regex from scratch.


Another Hidden Problem

Regex can also affect performance.

Some poorly written patterns force the computer to try an enormous number of possible matches before finally giving up.

This problem is known as catastrophic backtracking.

You don't need to understand the details yet.

Just remember this rule:

A shorter, simpler regex is usually better than a clever one that nobody understands.


Best Practices

At this point, we've covered the most important ideas about working with strings.

Here are two habits that will save you from many common bugs.

1. Trim User Input

Whenever text comes from a user, remove unnecessary spaces before using it.

const email = input.trim();
Enter fullscreen mode Exit fullscreen mode

This small step helps prevent failed comparisons, incorrect lookups, and frustrating bugs caused by invisible whitespace.


2. Avoid Repeated String Concatenation in Large Loops

For small pieces of text, using += is perfectly fine.

But when building very large strings inside loops, it's more efficient to collect the pieces first and join them once.

const parts = [];

for (const word of words) {
    parts.push(word);
}

const result = parts.join("");
Enter fullscreen mode Exit fullscreen mode

It's a simple change that scales much better as your program grows.


Common Beginner Mistakes

Let's finish by revisiting a few misconceptions that often catch new programmers off guard.

Mistake #1: Assuming length Means "Visible Characters"

For plain English text, that's usually true.

But once emojis or certain Unicode characters appear, it isn't.

"😀".length
Enter fullscreen mode Exit fullscreen mode

returns:

2
Enter fullscreen mode Exit fullscreen mode

because JavaScript counts UTF-16 code units, not the number of characters your eyes see.


Mistake #2: Forgetting That Strings Are Immutable

Methods like:

  • trim()
  • replace()
  • toUpperCase()

don't modify the existing string.

They return a new one.

If you ignore that returned value, nothing changes.


Mistake #3: Writing Regex Nobody Can Read

Regex is one of the most powerful tools in text processing.

It's also one of the easiest places to create confusing code.

If a regex becomes long or difficult to understand, add a comment explaining what it's supposed to match or consider whether there's a simpler approach.

Your future self (and your teammates) will thank you.


Wrapping Up

Strings seem simple because we use text every day.

But programming treats text very differently than humans do.

A string isn't just a sequence of letters. It's an immutable piece of data stored using an encoding, manipulated through specialized tools, and sometimes searched using powerful patterns.

Once you understand these ideas, many "weird" behaviors start making sense:

  • Why toUpperCase() doesn't change the original string.
  • Why one emoji can have a length of 2.
  • Why trimming user input prevents subtle bugs.
  • Why regex searches for patterns, not exact words.
  • Why repeatedly using += isn't always the best choice.

The next time you work with text, you'll know there's much more happening behind the scenes than meets the eye and you'll have the tools to handle it with confidence.

Top comments (1)

Collapse
 
merbayerp profile image
Mustafa ERBAY

I like that distinction. Events explain what happened, but not always why. For AI agents, that missing business intent is often more valuable than the event itself. I’d just be careful not to replace the specification with decisions. The specification should still describe the domain, while decisions become traceable artifacts derived from it. That’s what keeps both humans and agents aligned.