str.toUpperCase() returns a new uppercase string and leaves the original alone, because JavaScript strings are immutable. That covers the simple case; capitalising one letter, title-casing a sentence, and comparing strings without regard to case all need different approaches.
This is one of those methods that seems too simple to write about until you look at what people actually do with it — capitalise names, normalise input for comparison, title-case headings — and notice that each of those is a slightly different problem with a different correct answer.
Table of contents
- The method, and the return value people forget
- Capitalising the first letter
- Title case is not a string operation
- The Turkish i, and why case-insensitive comparison is subtle
- Where the case change should happen
- How this fits the rest of the stack
- FAQ
The method, and the return value people forget
const s = 'hello world';
s.toUpperCase(); // 'HELLO WORLD'
console.log(s); // 'hello world' -- unchanged
Strings are immutable in JavaScript. Every string method returns a new string, so a call whose result you do not assign has done nothing:
let name = 'sam';
name.toUpperCase(); // result discarded
console.log(name); // 'sam'
name = name.toUpperCase(); // correct
console.log(name); // 'SAM'
The counterpart is toLowerCase(), with identical semantics. Neither throws on non-alphabetic input — digits, punctuation and spaces pass through untouched — so there is no need to guard against them.
It does throw on null and undefined, which is the failure you will actually hit, usually against an optional field from an API:
const label = (user.nickname ?? '').toUpperCase();
// or
const label = user.nickname?.toUpperCase() ?? '';
The two differ in what they produce for a missing value — an empty string versus the fallback — so pick according to what the caller needs rather than by habit.
Capitalising the first letter
There is no built-in for this, which is a persistent minor annoyance. The idiom:
function capitalize(str) {
if (!str) return str;
return str[0].toUpperCase() + str.slice(1);
}
capitalize('hello world'); // 'Hello world'
Note it leaves the rest alone. If your input might be shouting, lowercase the remainder first:
function capitalize(str) {
if (!str) return str;
return str[0].toUpperCase() + str.slice(1).toLowerCase();
}
capitalize('HELLO WORLD'); // 'Hello world'
Which of those you want depends entirely on the data, and choosing wrong is how iPhone becomes Iphone.
The indexing has the same UTF-16 problem as any other string index: str[0] is a code unit, not a character, so a string starting with an emoji or certain scripts gets cut in half. For user-supplied text, work in graphemes:
function capitalize(str) {
if (!str) return str;
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
const [first] = seg.segment(str);
return first.segment.toUpperCase() + str.slice(first.segment.length);
}
And if the capitalisation is purely visual, do it in CSS instead. text-transform: capitalize leaves your data intact, which matters because a name stored capitalised is a name you have modified.
Title case is not a string operation
The obvious approach works on obvious input:
function titleCase(str) {
return str
.toLowerCase()
.split(' ')
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ');
}
titleCase('the quick brown fox'); // 'The Quick Brown Fox'
And then reality arrives. mcdonald becomes Mcdonald. o'brien becomes O'brien. jean-luc becomes Jean-luc. iPhone becomes Iphone. IBM becomes Ibm.
Splitting on hyphens and apostrophes fixes some of it and breaks other things — real style guides lowercase short prepositions and articles except at the start, and there is no algorithm that knows van der Berg from Van Der Berg without knowing whose name it is.
The practical conclusions:
- For names, do not transform at all. Store what the user typed and display it. People know how their own name is capitalised, and they notice when a system disagrees.
- For headings, use CSS.
text-transform: capitalizeis a display decision that leaves the underlying text unchanged. - For editorial title case with real style rules, use a library. The rules are genuinely intricate and not worth reimplementing.
- For slugs and identifiers, lowercase everything and stop worrying about it.
The Turkish i, and why case-insensitive comparison is subtle
The natural way to compare strings without regard to case is to uppercase both. In one widely-used locale, that is wrong.
Turkish has two distinct letters: dotted i and dotless ı. The uppercase of dotted i is İ — with a dot — not I. So under a Turkish locale, uppercasing i does not give you I, and a comparison built on that assumption fails.
'i'.toUpperCase(); // 'I'
'i'.toLocaleUpperCase('tr-TR'); // 'İ'
'I'.toLocaleLowerCase('tr-TR'); // 'ı'
This is not a hypothetical. It has broken real systems, classically when a locale-sensitive uppercase was applied to a protocol keyword or a filename and produced a string the rest of the system did not recognise.
Two rules that resolve it:
- For anything machine-facing — comparing identifiers, normalising a header name, building a lookup key — use
toUpperCase()andtoLowerCase(), which are locale-independent by design. - For anything shown to a user — a heading, a label, a name in their own language — use
toLocaleUpperCase(locale)so it is correct for them.
For comparison specifically, neither is really the right tool. Use the built-in collator, which handles case, accents and locale ordering properly:
const eq = new Intl.Collator('en', { sensitivity: 'base' });
eq.compare('café', 'CAFE') === 0; // true -- ignores case and accents
eq.compare('straße', 'STRASSE') === 0; // true
That German example is the one that defeats an uppercase-and-compare approach entirely: ß uppercases to two characters, so the strings have different lengths and a naive comparison fails.
Where the case change should happen
A recurring bug class is casing applied in the wrong layer. Some rules that hold up:
- Never uppercase data before storing it. You cannot recover the original, and someone will eventually need it. Store as entered; transform on display.
- Normalise for comparison, not for storage. Email addresses are the classic case: store what the user typed, but compare on a lowercased form so
Sam@Example.comandsam@example.comare the same account. Many systems keep a separate normalised column for exactly this. - Do visual casing in CSS. A heading in capitals is a design decision, and design decisions belong in stylesheets where they can change without a data migration.
- Be careful with locale-sensitive methods on the server. The runtime’s default locale may differ from your development machine, which is how output changes between environments for no visible reason.
That last point is worth a moment. A server whose default locale differs from your laptop will format and case differently, and the symptom is a bug that reproduces in production and nowhere else. Pass the locale explicitly rather than relying on a default you did not choose.
How this fits the rest of the stack
Case conversion looks like the simplest thing in the standard library, and most of its bugs come from applying it at the wrong layer — to stored data instead of displayed text, or with a locale-sensitive method on a machine-facing string.
The environment-dependent version of that is a genuine deployment problem: code that cases correctly on your machine behaving differently on a server with a different default locale. Seeing what the server actually produced is what makes that quick to diagnose. On RunxBuild, a service in Node, Next.js, Python, Go, Ruby, Java, .NET or Docker deploys from your GitHub repository with runtime logs and build logs in one place, environment variables as service settings rather than machine configuration, and rollback to the previous deploy. Managed MySQL and Postgres sit alongside on private networking. To see what a service and its database add up to, the RunxBuild hosting calculator lists them as separate line items.
Useful related references:
- JavaScript Sleep: Why There Is No sleep() and What To Use Instead
- How to Disable JavaScript in Chrome, and Why You Should Do It on Purpose
- Building a Password Generator in JavaScript, Correctly
- Services on RunxBuild
FAQ
How do I convert a string to uppercase in JavaScript?
Call toUpperCase() on it. Strings are immutable, so the method returns a new string and leaves the original unchanged — you must assign the result. It throws on null and undefined, so use optional chaining or a nullish default when the value might be missing.
How do I capitalise just the first letter?
There is no built-in. Take the first character, uppercase it, and concatenate the rest: str[0].toUpperCase() + str.slice(1). Decide deliberately whether to lowercase the remainder, since doing so turns iPhone into Iphone. If the change is purely visual, text-transform: capitalize in CSS leaves your data intact.
Why does toUpperCase behave differently in Turkish?
Turkish has dotted and dotless i as separate letters, and the uppercase of dotted i is İ rather than I. toLocaleUpperCase('tr-TR') reflects that; toUpperCase() is locale-independent. Use the locale-independent form for identifiers and comparisons, and the locale-aware form for text shown to a user.
How do I compare two strings ignoring case?
Use Intl.Collator with sensitivity: 'base', which ignores case and accents and handles locale rules properly. Uppercasing both strings and comparing fails on characters like German ß, which uppercases to two letters and produces strings of different lengths.
Should I store data in uppercase?
No. Store what the user entered and transform on display, because an uppercased value cannot be converted back. Where you need case-insensitive matching — email addresses are the usual example — keep a separate normalised column for comparison alongside the original.