charAt()
ES3+Returns a new string containing the single UTF-16 code unit at the given index, or an empty string when the index is out of range. With no argument it returns the first character. For characters beyond the Basic Multilingual Plane, such as emoji, it yields one half of a surrogate pair rather than the full symbol.
Syntax
string.charAt(index)Parameters
index number The index of the character to return
Return Value
The character at the specified index
Examples
const str = 'Hello';
console.log(str.charAt(0));
console.log(str.charAt(4)); 📌 When to Use
Use charAt() to pick out single characters by position in text you know is simple: taking the first letter of a word to capitalize it, building initials from names, examining one character at a time in a manual scan, or masking all but the last digits of a card number. Its distinguishing behavior versus bracket indexing is the out-of-range result: charAt(99) on a short string returns an empty string, which concatenates harmlessly and keeps string operations total, while str[99] returns undefined, which becomes the text 'undefined' if it reaches concatenation, a much louder failure. Which failure you prefer is a legitimate choice: the empty string is forgiving in display code, while undefined surfaces bugs earlier in logic code. Modern alternatives cover its weak spots: at() accepts negative indices, so str.at(-1) reads the last character without length arithmetic, and codePointAt() with for-of iteration handles full code points, which charAt() cannot, since it operates on UTF-16 code units and will hand you half of an emoji. For heavy character-by-character work over large strings, comparing numeric codes from charCodeAt() avoids allocating a one-character string per step. Reserve charAt() for readable, occasional character access on predominantly BMP text like names, identifiers, and ASCII data.
⚠️ Common Mistakes
Getting half an emoji: characters outside the Basic Multilingual Plane occupy two UTF-16 code units, and charAt() returns exactly one, a lone surrogate that renders as a broken glyph. Taking 'the first character' of user-generated text needs code-point awareness: for-of, the spread operator, or codePointAt().
Assuming an out-of-range access fails visibly: charAt() returns an empty string for any invalid index, so off-by-one errors produce silently shorter output rather than an exception, and can go unnoticed for a long time.
Confusing charAt() with charCodeAt(): the former returns a one-character string, the latter a number. Comparing charAt(i) >= 65 compares a string with a number through coercion and yields nonsense; use charCodeAt() when you mean to work with numeric codes.
Expecting negative indices to count from the end: charAt(-1) returns an empty string, not the last character. That job belongs to at(-1), or to charAt(str.length - 1) in older codebases.
Using an index that is not an integer: the argument is truncated toward zero, so charAt(1.9) reads index 1 and NaN reads index 0. Indices produced by division or parsing should be floored explicitly so the rounding is visible in the code.
Building per-character loops on charAt() for perceived characters: iterating i from 0 to length - 1 walks code units, not user-perceived characters, splitting surrogate pairs and combining sequences. Grapheme-true iteration needs Intl.Segmenter.
✅ Best Practices
Choose the access style by the failure mode you want: charAt() degrades to an empty string, bracket indexing surfaces undefined, and at() adds negative indexing; be consistent within a codebase.
Use at(-1) for last-character access in modern targets instead of the charAt(str.length - 1) arithmetic.
Guard against empty strings before reading the first character, as in word.length === 0, so capitalize-first-letter helpers do not quietly emit unchanged empty words.
For user-generated text that may contain emoji, take the first grapheme with the spread operator or Intl.Segmenter rather than charAt(0), so profile initials and truncations never show broken glyphs.
In performance-sensitive scans, compare charCodeAt() numbers instead of charAt() strings, avoiding a string allocation per character examined.
Prefer whole-string methods, such as slice(), toUpperCase() on slices, and regex replacements, over assembling results character by character with charAt(); they are clearer and faster.
⚡ Performance Notes
charAt() itself is constant-time, but it allocates a new one-character string per call, and engines only partially mitigate this with caches for common single characters. In character-by-character processing of large strings, that per-step allocation is the dominant cost, which is why hot loops compare numeric codes from charCodeAt() or codePointAt() instead, deferring any string creation until a result is actually assembled. Bracket indexing has the same allocation profile as charAt(), so the choice between them is about semantics, not speed. Also prefer whole-string operations where possible: one slice() plus one toUpperCase() beats a loop of charAt() calls both in clarity and in allocation count. For occasional access, such as reading the first letter of each word in a name, none of this is measurable, and readability should decide.
🌍 Real World Example
Character Analysis and String Transformation
Three single-character utilities. capitalizeWords() splits a phrase into words and rebuilds each as charAt(0).toUpperCase() plus the lowercased remainder from slice(1), the textbook title-case recipe, with an empty-word guard so double spaces cannot crash it. getInitials() maps each name part to its first character, uppercased and joined, turning 'John Michael Doe' into 'JMD'. maskNumber() walks a card number with charAt() to replace digits with asterisks while preserving the spacing, then appends the last four characters intact, the familiar payment-UI masking pattern. All three operate on names and digits, BMP text where charAt() is entirely safe.
// Capitalize first letter of each word
function capitalizeWords(str) {
return str.split(' ').map(word => {
if (word.length === 0) return word;
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}).join(' ');
}
console.log(capitalizeWords('hello world')); // 'Hello World'
// Generate initials from full name
function getInitials(fullName) {
return fullName
.split(' ')
.filter(word => word.length > 0)
.map(word => word.charAt(0).toUpperCase())
.join('');
}
console.log(getInitials('John Michael Doe')); // 'JMD'
// Mask sensitive data (credit card, etc.)
function maskNumber(number, visibleChars = 4) {
const str = String(number);
if (str.length <= visibleChars) return str;
let masked = '';
for (let i = 0; i < str.length - visibleChars; i++) {
masked += str.charAt(i) === ' ' ? ' ' : '*';
}
return masked + str.slice(-visibleChars);
}
console.log(maskNumber('1234 5678 9012 3456'));
// '**** **** **** 3456'