JavaScript's substring() method extracts characters between two index positions in a string and returns the result as a new string. It is one of the most commonly used string methods in the language, appearing in everything from URL parsing to input validation to display text truncation. Despite being straightforward in concept, substring() has several behavioral quirks -- particularly around negative indices and argument swapping -- that trip up even experienced developers.
This guide covers the full substring() API with visual indexing examples, a detailed comparison with slice() and the deprecated substr(), TypeScript type patterns, real-world use cases, common mistakes, and performance considerations.
What Is substring()?
The substring() method extracts a portion of a string between a start index and an end index, returning the extracted part as a new string. The original string is never modified -- strings in JavaScript are immutable.
Syntax
string.substring(indexStart)
string.substring(indexStart, indexEnd)
Parameters:
-
indexStart-- The index of the first character to include in the returned substring. -
indexEnd(optional) -- The index of the first character to exclude from the returned substring. If omitted,substring()extracts characters to the end of the string.
Return value: A new string containing the extracted section of the original string.
Basic example
const language = "JavaScript";
console.log(language.substring(0, 4)); // "Java"
console.log(language.substring(4)); // "Script"
console.log(language.substring(4, 10)); // "Script"
The key thing to internalize is that indexEnd is exclusive. When you call substring(0, 4), you get characters at indices 0, 1, 2, and 3 -- but not index 4. This is consistent with how most range-based APIs work in JavaScript (think Array.prototype.slice, for loops with i < length, etc.).
How substring() handles edge cases
substring() has several implicit behaviors that make it more forgiving than slice():
const str = "Hello, World!";
// If indexStart equals indexEnd, returns empty string
console.log(str.substring(5, 5)); // ""
// If indexStart > indexEnd, arguments are swapped
console.log(str.substring(5, 0)); // "Hello" (same as substring(0, 5))
// If either argument is NaN or negative, it is treated as 0
console.log(str.substring(-3)); // "Hello, World!" (same as substring(0))
console.log(str.substring(NaN)); // "Hello, World!" (same as substring(0))
// If either argument > string length, it is treated as string.length
console.log(str.substring(0, 100)); // "Hello, World!"
The argument-swapping behavior is unique to substring(). Neither slice() nor the deprecated substr() does this.
Visual Indexing Guide
Understanding string indexing is critical to using substring() correctly. Every character in a JavaScript string has a zero-based index position. Here is how the string "JavaScript" maps to its indices:
J a v a S c r i p t
0 1 2 3 4 5 6 7 8 9
When you call substring(indexStart, indexEnd), think of the indices as pointing to the gaps between characters:
|J|a|v|a|S|c|r|i|p|t|
0 1 2 3 4 5 6 7 8 9 10
So substring(0, 4) captures everything between position 0 and position 4:
|J|a|v|a|S|c|r|i|p|t|
0 1 2 3 4 5 6 7 8 9 10
^-------^
"Java"
And substring(4, 10) captures everything from position 4 to position 10:
|J|a|v|a|S|c|r|i|p|t|
0 1 2 3 4 5 6 7 8 9 10
^-----------^
"Script"
This mental model helps avoid the most common off-by-one errors. The length of the returned substring is always indexEnd - indexStart.
const word = "JavaScript";
// Length of result = indexEnd - indexStart
console.log(word.substring(0, 4).length); // 4 (4 - 0)
console.log(word.substring(4, 10).length); // 6 (10 - 4)
console.log(word.substring(2, 7).length); // 5 (7 - 2)
Core Examples
Basic extraction
const greeting = "Hello, World!";
// Extract the first word
console.log(greeting.substring(0, 5)); // "Hello"
// Extract after the comma and space
console.log(greeting.substring(7, 12)); // "World"
// Extract a single character (same as charAt)
console.log(greeting.substring(0, 1)); // "H"
Omitting the second parameter
When you omit indexEnd, substring() extracts from indexStart to the end of the string:
const path = "/users/profile/settings";
// Everything after the first slash
console.log(path.substring(1)); // "users/profile/settings"
// Everything after index 6
console.log(path.substring(7)); // "profile/settings"
// Everything from the last segment
const lastSlash = path.lastIndexOf("/");
console.log(path.substring(lastSlash + 1)); // "settings"
Extracting from the middle
Combining indexOf() with substring() is a common pattern for extracting specific parts of a string:
const email = "developer@example.com";
// Extract the part before the @
const atIndex = email.indexOf("@");
const username = email.substring(0, atIndex);
console.log(username); // "developer"
// Extract the domain
const domain = email.substring(atIndex + 1);
console.log(domain); // "example.com"
// Extract just the domain name without TLD
const dotIndex = email.lastIndexOf(".");
const domainName = email.substring(atIndex + 1, dotIndex);
console.log(domainName); // "example"
Dynamic extraction with variables
function extractBetween(str, startChar, endChar) {
const startIndex = str.indexOf(startChar);
const endIndex = str.indexOf(endChar, startIndex + 1);
if (startIndex === -1 || endIndex === -1) return "";
return str.substring(startIndex + 1, endIndex);
}
console.log(extractBetween("Hello [World]!", "[", "]")); // "World"
console.log(extractBetween("<title>My Page</title>", ">", "<")); // "My Page"
substring() vs slice() vs substr()
JavaScript has three methods for extracting parts of a string. This is a frequent source of confusion, so here is the definitive comparison.
Comparison table
| Feature | substring(start, end) |
slice(start, end) |
substr(start, length) |
|---|---|---|---|
| Second parameter meaning | End index (exclusive) | End index (exclusive) | Length of extraction |
| Negative start | Treated as 0
|
Counts from end of string | Counts from end of string |
| Negative end | Treated as 0
|
Counts from end of string | N/A (it is a length) |
| Start > end | Swaps arguments | Returns ""
|
N/A |
| NaN arguments | Treated as 0
|
Treated as 0
|
Treated as 0
|
| Status | Standard | Standard | Legacy (Annex B) |
Negative index behavior
This is the most important difference. slice() supports negative indices to count from the end of a string. substring() clamps negatives to zero.
const str = "JavaScript";
// Getting the last 6 characters
console.log(str.slice(-6)); // "Script" -- counts 6 from the end
console.log(str.substring(-6)); // "JavaScript" -- -6 becomes 0, same as substring(0)
// Getting characters from index 0 to 6 from the end
console.log(str.slice(0, -6)); // "Java" -- end becomes 10 - 6 = 4
console.log(str.substring(0, -6)); // "" -- -6 becomes 0, then substring(0, 0)
Argument swapping
substring() silently swaps its arguments when start > end. slice() does not.
const str = "JavaScript";
// When start > end
console.log(str.substring(6, 2)); // "vaSc" -- swapped to substring(2, 6)
console.log(str.slice(6, 2)); // "" -- returns empty string, no swap
This swapping behavior can mask bugs. If you accidentally pass arguments in the wrong order, substring() will silently "fix" it and return a result that may look correct but is actually hiding an error in your logic.
The deprecated substr()
substr() is fundamentally different because its second parameter is a length, not an end index:
const str = "JavaScript";
// substr(start, length) vs substring(start, end)
console.log(str.substr(4, 6)); // "Script" -- start at 4, take 6 characters
console.log(str.substring(4, 6)); // "Sc" -- start at 4, end before 6
// substr supports negative start index
console.log(str.substr(-6, 6)); // "Script" -- start 6 from end, take 6
substr() is defined in Annex B of the ECMAScript specification, which means it exists only for backward compatibility with legacy web content. It is not part of the core language standard. Do not use substr() in new code. Use substring() or slice() instead.
When to use which
Use slice() as your default. It handles negative indices intuitively, does not silently swap arguments, and has the same API as Array.prototype.slice() -- so developers already have the correct mental model for it.
Use substring() when you specifically want the argument-swapping behavior, or when you are working with code that already uses it and consistency matters.
Never use substr(). It is deprecated. If you encounter it in existing code, refactor to slice() or substring().
// Preferred: slice()
const filename = "document.pdf";
const extension = filename.slice(-3); // "pdf"
const name = filename.slice(0, -4); // "document"
// Also fine: substring() when you have explicit indices
const url = "https://example.com/path";
const protocol = url.substring(0, url.indexOf("://")); // "https"
// Avoid: substr()
const legacy = filename.substr(0, 8); // "document" -- refactor this
TypeScript Usage
substring() works identically in TypeScript at runtime, but TypeScript adds compile-time type information that helps prevent errors.
Type signatures
TypeScript defines substring() on the String interface:
interface String {
substring(start: number, end?: number): string;
}
Both parameters are typed as number, and the return type is always string. TypeScript will catch type errors at compile time:
const text: string = "Hello, TypeScript!";
// These work fine
const hello: string = text.substring(0, 5);
const typescript: string = text.substring(7, 17);
// TypeScript catches these errors
// text.substring("0", "5"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
// text.substring(true); // Error: Argument of type 'boolean' is not assignable to parameter of type 'number'
Using substring() with template literal types
TypeScript 4.1 introduced template literal types, which allow string manipulation at the type level. While substring() itself does not have a type-level equivalent, you can build utility types that operate on string types:
// Extract the first N characters at the type level
type FirstChar<S extends string> = S extends `${infer First}${string}` ? First : never;
type Result = FirstChar<"Hello">; // "H"
// Split a string type at a delimiter
type Before<S extends string, D extends string> =
S extends `${infer Head}${D}${string}` ? Head : S;
type After<S extends string, D extends string> =
S extends `${string}${D}${infer Tail}` ? Tail : never;
type Domain = After<"user@example.com", "@">; // "example.com"
type Username = Before<"user@example.com", "@">; // "user"
Practical TypeScript patterns
When using substring() in TypeScript, leverage type narrowing and null checks:
function extractDomain(email: string): string | null {
const atIndex = email.indexOf("@");
if (atIndex === -1) return null;
return email.substring(atIndex + 1);
}
function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
}
// Type-safe URL parsing
interface ParsedURL {
protocol: string;
host: string;
path: string;
}
function parseSimpleURL(url: string): ParsedURL | null {
const protocolEnd = url.indexOf("://");
if (protocolEnd === -1) return null;
const protocol = url.substring(0, protocolEnd);
const rest = url.substring(protocolEnd + 3);
const pathStart = rest.indexOf("/");
if (pathStart === -1) {
return { protocol, host: rest, path: "/" };
}
return {
protocol,
host: rest.substring(0, pathStart),
path: rest.substring(pathStart),
};
}
const result = parseSimpleURL("https://example.com/blog/post");
// { protocol: "https", host: "example.com", path: "/blog/post" }
Real-World Use Cases
URL parsing
Extracting components from URLs is one of the most common uses of substring(). While the URL constructor is the preferred approach for full URL parsing, substring() is useful for quick extractions and situations where the URL API is not available or appropriate:
const url = "https://example.com/blog/posts?page=2&sort=date#comments";
// Extract protocol
const protocol = url.substring(0, url.indexOf("://"));
console.log(protocol); // "https"
// Extract hostname
const afterProtocol = url.substring(url.indexOf("://") + 3);
const hostname = afterProtocol.substring(0, afterProtocol.indexOf("/"));
console.log(hostname); // "example.com"
// Extract path (without query string)
const pathStart = url.indexOf("/", url.indexOf("://") + 3);
const queryStart = url.indexOf("?");
const path = url.substring(pathStart, queryStart !== -1 ? queryStart : url.length);
console.log(path); // "/blog/posts"
// Extract query string
const queryString = url.substring(url.indexOf("?") + 1, url.indexOf("#"));
console.log(queryString); // "page=2&sort=date"
// Extract hash/fragment
const hash = url.substring(url.indexOf("#") + 1);
console.log(hash); // "comments"
File extension extraction
function getFileExtension(filename) {
const dotIndex = filename.lastIndexOf(".");
if (dotIndex === -1 || dotIndex === 0) return "";
return filename.substring(dotIndex + 1);
}
console.log(getFileExtension("photo.jpg")); // "jpg"
console.log(getFileExtension("archive.tar.gz")); // "gz"
console.log(getFileExtension("README")); // ""
console.log(getFileExtension(".gitignore")); // "gitignore"
// Extracting filename without extension
function getFilename(filepath) {
// Remove path
const lastSlash = filepath.lastIndexOf("/");
const filename = lastSlash !== -1 ? filepath.substring(lastSlash + 1) : filepath;
// Remove extension
const dotIndex = filename.lastIndexOf(".");
if (dotIndex === -1) return filename;
return filename.substring(0, dotIndex);
}
console.log(getFilename("/path/to/document.pdf")); // "document"
console.log(getFilename("image.png")); // "image"
Form validation and input limiting
function enforceMaxLength(input, maxLength) {
if (input.length > maxLength) {
return input.substring(0, maxLength);
}
return input;
}
// Credit card formatting - show only last 4 digits
function maskCreditCard(cardNumber) {
const cleaned = cardNumber.replace(/\s/g, "");
if (cleaned.length < 4) return cleaned;
const lastFour = cleaned.substring(cleaned.length - 4);
return "**** **** **** " + lastFour;
}
console.log(maskCreditCard("4111 1111 1111 1234")); // "**** **** **** 1234"
// Phone number formatting
function formatPhoneNumber(phone) {
const digits = phone.replace(/\D/g, "");
if (digits.length !== 10) return phone;
return (
"(" +
digits.substring(0, 3) +
") " +
digits.substring(3, 6) +
"-" +
digits.substring(6)
);
}
console.log(formatPhoneNumber("5551234567")); // "(555) 123-4567"
Truncating display text
function truncate(text, maxLength, suffix = "...") {
if (text.length <= maxLength) return text;
// Truncate at the last space before maxLength to avoid cutting words
const truncated = text.substring(0, maxLength - suffix.length);
const lastSpace = truncated.lastIndexOf(" ");
if (lastSpace === -1) {
return truncated + suffix;
}
return truncated.substring(0, lastSpace) + suffix;
}
const paragraph =
"JavaScript is a versatile programming language used for both client-side and server-side development.";
console.log(truncate(paragraph, 50));
// "JavaScript is a versatile programming..."
console.log(truncate(paragraph, 30));
// "JavaScript is a versatile..."
Email domain extraction
function getEmailParts(email) {
const atIndex = email.indexOf("@");
if (atIndex === -1) {
throw new Error("Invalid email: missing @ symbol");
}
const localPart = email.substring(0, atIndex);
const domain = email.substring(atIndex + 1);
const dotIndex = domain.lastIndexOf(".");
const domainName = domain.substring(0, dotIndex);
const tld = domain.substring(dotIndex + 1);
return { localPart, domain, domainName, tld };
}
console.log(getEmailParts("admin@mail.example.com"));
// {
// localPart: "admin",
// domain: "mail.example.com",
// domainName: "mail.example",
// tld: "com"
// }
Parsing structured strings
// Parse a CSV row (simplified, does not handle quoted fields)
function parseCSVRow(row) {
const fields = [];
let current = 0;
while (current < row.length) {
const nextComma = row.indexOf(",", current);
if (nextComma === -1) {
fields.push(row.substring(current));
break;
}
fields.push(row.substring(current, nextComma));
current = nextComma + 1;
}
return fields;
}
console.log(parseCSVRow("John,Doe,35,Engineer"));
// ["John", "Doe", "35", "Engineer"]
// Extract key-value from a config line
function parseConfigLine(line) {
const equalsIndex = line.indexOf("=");
if (equalsIndex === -1) return null;
const key = line.substring(0, equalsIndex).trim();
const value = line.substring(equalsIndex + 1).trim();
return { key, value };
}
console.log(parseConfigLine("DATABASE_URL = postgres://localhost:5432/mydb"));
// { key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" }
Common Mistakes
Confusing substring() and slice() with negative indices
This is the single most common mistake. Developers assume substring() handles negative indices the same way slice() does.
const str = "JavaScript";
// WRONG: expecting last 3 characters
const wrong = str.substring(-3);
console.log(wrong); // "JavaScript" -- got the entire string!
// CORRECT: use slice() for negative indices
const correct = str.slice(-3);
console.log(correct); // "ipt"
// CORRECT: use manual calculation with substring()
const alsoCorrect = str.substring(str.length - 3);
console.log(alsoCorrect); // "ipt"
Off-by-one errors
Forgetting that indexEnd is exclusive leads to extracting one character too many or too few:
const str = "abcdef";
// WRONG: trying to extract "bcd" (3 characters starting at index 1)
const wrong = str.substring(1, 3); // "bc" -- only 2 characters!
// CORRECT: remember indexEnd is exclusive
const correct = str.substring(1, 4); // "bcd"
// TIP: the result length is always (indexEnd - indexStart)
// To extract N characters starting at position P: substring(P, P + N)
Assuming substring() mutates the string
Strings in JavaScript are immutable. Every string method returns a new string.
const original = "Hello, World!";
const extracted = original.substring(0, 5);
console.log(extracted); // "Hello"
console.log(original); // "Hello, World!" -- unchanged!
// If you want to "modify" a string, reassign the variable
let mutable = "Hello, World!";
mutable = mutable.substring(0, 5) + " JavaScript!";
console.log(mutable); // "Hello JavaScript!"
Using the deprecated substr()
Many online tutorials and older codebases still use substr(). Always refactor to substring() or slice():
// AVOID: substr(start, length)
const str = "JavaScript";
const old = str.substr(4, 6); // "Script"
// PREFER: slice(start, end) or substring(start, end)
const modern = str.slice(4, 10); // "Script"
const alsoModern = str.substring(4, 10); // "Script"
Not handling indexOf() returning -1
When using indexOf() to find a position for substring(), forgetting to check for -1 produces unexpected results:
const str = "hello world";
// WRONG: if "@" is not found, indexOf returns -1
const domain = str.substring(str.indexOf("@") + 1);
console.log(domain); // "hello world" -- not what you expected
// CORRECT: always check for -1
function safeDomainExtract(email) {
const atIndex = email.indexOf("@");
if (atIndex === -1) return null;
return email.substring(atIndex + 1);
}
Performance Considerations
Substring creation in V8
JavaScript engines like V8 (Chrome, Node.js) have internal optimizations for string operations. When you call substring(), V8 does not always allocate a brand new copy of the characters. For larger strings, V8 may create a "sliced string" -- an internal representation that references the original string's character buffer with an offset and length. This makes substring() an O(1) operation in many cases rather than O(n).
// V8 may internally represent this as a reference to the original
const massive = "a".repeat(1_000_000);
const tiny = massive.substring(0, 10); // O(1) in V8 -- no copy needed
However, this optimization has a trade-off. The sliced string keeps a reference to the original string, preventing it from being garbage collected. If you take a small substring from a very large string and then discard the original, the large string remains in memory.
// Potential memory issue
function getHeader(response) {
// response is a 10MB string
// This tiny substring keeps the entire 10MB string alive in V8
return response.substring(0, 50);
}
// Fix: force a copy by concatenating with empty string
function getHeaderSafe(response) {
return ("" + response.substring(0, 50));
}
In practice, this matters only when dealing with very large strings (multiple megabytes) where a small extraction is kept long after the original should be freed. For typical web application string operations, this is not a concern.
Avoiding unnecessary allocations in loops
When processing strings in tight loops, repeated substring() calls create intermediate string objects. For performance-critical code, consider alternatives:
// SLOW: creates many intermediate strings
function countOccurrences(str, char) {
let count = 0;
let remaining = str;
while (remaining.length > 0) {
if (remaining.substring(0, 1) === char) count++;
remaining = remaining.substring(1); // new string every iteration
}
return count;
}
// FAST: use charAt() or index access instead
function countOccurrencesFast(str, char) {
let count = 0;
for (let i = 0; i < str.length; i++) {
if (str[i] === char) count++;
}
return count;
}
// FASTEST: use a built-in method
function countOccurrencesBuiltin(str, char) {
return str.split(char).length - 1;
}
String interning
JavaScript engines intern (deduplicate) certain strings, especially string literals and property names. However, strings created by substring() are generally not interned. This means two substring() calls that produce identical strings will create separate string objects:
const str = "Hello";
const a = str.substring(0, 3); // "Hel"
const b = str.substring(0, 3); // "Hel" -- separate object from a
// Strict equality still works (compares values, not references)
console.log(a === b); // true
This is rarely a concern for application code, but it matters in scenarios where you are creating millions of string operations per second, such as parsers or data transformation pipelines. In such cases, consider caching extracted substrings in a Map if the same extractions are repeated.
Browser Compatibility
substring() has universal browser support. It is part of the original JavaScript specification (ES1, 1997) and is available in every environment that runs JavaScript:
- All modern browsers (Chrome, Firefox, Safari, Edge)
- Internet Explorer (all versions)
- Node.js (all versions)
- Deno, Bun, and all other JavaScript runtimes
There are no compatibility concerns with substring(). It works everywhere JavaScript runs.
A note on substr() deprecation
While substring() is fully standardized and will never be removed, substr() has a different status. substr() is defined in Annex B of the ECMAScript specification, titled "Additional ECMAScript Features for Web Browsers." This annex exists for backward compatibility only. The specification explicitly states that these features are not considered part of the core language and may not be present in non-browser JavaScript implementations.
In practice, substr() works in all current environments, but:
- It could be removed from non-browser runtimes at any time without violating the specification.
- TypeScript and linting tools increasingly flag its usage as deprecated.
- MDN documentation marks it with a deprecation warning.
The migration from substr() to substring() or slice() is straightforward:
// substr(start, length) -> slice(start, start + length)
const str = "JavaScript";
// Before (deprecated)
str.substr(4, 6); // "Script"
// After (using slice)
str.slice(4, 4 + 6); // "Script"
// After (using substring)
str.substring(4, 4 + 6); // "Script"
// For negative start: substr(-6, 6) -> slice(-6)
str.substr(-6, 6); // "Script"
str.slice(-6); // "Script"
Summary
substring() is a foundational string method that every JavaScript developer should understand thoroughly. Its API is simple -- pass a start index and an optional end index to extract part of a string -- but its edge case behaviors around negative indices and argument swapping set it apart from slice().
For new code, slice() is generally the better choice. It handles negative indices intuitively, does not silently swap arguments, and shares its API with Array.prototype.slice(). Use substring() when working with codebases that already use it, when you want the argument-swapping behavior, or when you are working with explicitly calculated positive indices where the behavioral differences do not matter. Avoid substr() entirely -- it is deprecated and its length-based second parameter is a different paradigm that creates confusion when mixed with substring() and slice().
Frequently Asked Questions
What is the difference between substring() and slice() in JavaScript?
Both extract portions of a string, but they handle negative indices differently. slice() supports negative indices (counting from the end), while substring() treats negative values as 0. slice(-3) returns the last 3 characters; substring(-3) returns the entire string. For most use cases, slice() is preferred due to its more predictable behavior with negative indices.
Is substr() deprecated in JavaScript?
Yes, substr() is considered a legacy method and is defined in Annex B of the ECMAScript specification, meaning it exists only for web compatibility. Use substring() or slice() instead. The key difference is that substr(start, length) takes a length parameter, while substring(start, end) and slice(start, end) take an end index.
How do I get the last N characters of a string in JavaScript?
Use slice() with a negative index: str.slice(-3) returns the last 3 characters. substring() cannot do this directly since it treats negative values as 0. Alternatively, use str.substring(str.length - 3) to achieve the same result with substring().
Does substring() modify the original string?
No. Strings in JavaScript are immutable. substring() returns a new string without modifying the original. All string methods in JavaScript return new strings rather than mutating the existing one.
What happens if start is greater than end in substring()?
Unlike slice(), substring() swaps the arguments if start is greater than end. So str.substring(5, 2) is equivalent to str.substring(2, 5). This auto-swapping behavior is unique to substring() — slice() would return an empty string in the same scenario.
Originally published at aicodereview.cc
Top comments (0)