- Published on
slice() vs substring() vs substr(): Complete JavaScript String Methods Comparison
- Authors

- Name
- Daniel Danielecki
- @ddanielecki

Table of Contents
- Introduction
- Understanding slice() / String.prototype.slice()
- Understanding substring() / String.prototype.substring()
- Understanding substr() / String.prototype.substr() (Deprecated)
- Side-by-Side Comparison
- Real-World Use Cases
- Use Case 1: Extracting File Extensions
- Use Case 2: Truncating Text with Ellipsis
- Use Case 3: Extracting Domain from URL
- Use Case 4: Getting Last N Characters
- Use Case 5: Masking Sensitive Information
- Use Case 6: Parsing and Formatting Dates
- Use Case 7: Text Processing and Word Extraction
- Use Case 8: URL and Path Manipulation
- Use Case 9: String Validation and Sanitization
- Common Pitfalls and Mistakes
- Best Practices
- Performance Considerations
- Browser Compatibility
- Further Learning Resources
- Conclusion
- Quick Comparison Table
Introduction
When working with JavaScript strings, developers often encounter three similar methods for extracting substrings: String.prototype.slice(), String.prototype.substring(), and String.prototype.substr() (commonly used as slice(), substring(), and substr()). While they might seem interchangeable at first glance, each method has distinct behaviors that can lead to unexpected results if not understood properly.
This comprehensive guide will provide an in-depth exploration of each method, covering:
- Detailed syntax and parameter behavior for each method
- Complete algorithm explanations showing how each method processes indices
- Comprehensive examples covering all edge cases and boundary conditions
- Real-world use cases with production-ready code examples
- Common pitfalls and debugging strategies to help you avoid mistakes
- Performance analysis and optimization considerations
- Type coercion behavior and how non-number values are handled
- Unicode and special character handling for international strings
- Integration patterns with other JavaScript string methods
By the end of this article, you'll not only understand each method's behavior but also gain the expertise to choose the right method for your specific use case, write more robust code, and debug string manipulation issues effectively.
Understanding slice() / String.prototype.slice()
The String.prototype.slice() method (commonly called slice()) extracts a section of a string and returns it as a new string, without modifying the original string. It's the most modern and recommended method for extracting substrings in JavaScript. slice() was introduced in ECMAScript 3 and is part of the official JavaScript specification, making it the standard choice for string extraction operations.
Syntax
string.slice(startIndex, endIndex)
Parameters
startIndex: The zero-based index at which to begin extraction. Can be negative (counts from end). If negative, it's converted tostring.length + startIndex. If undefined, defaults to 0. If greater thanstring.length, it's clamped tostring.length.endIndex(optional): The zero-based index before which to end extraction (exclusive). Can be negative (counts from end). If negative, it's converted tostring.length + endIndex. If omitted or undefined, defaults tostring.length. If greater thanstring.length, it's clamped tostring.length.
Key Features of slice()
- Supports Negative Indices: Negative indices count backwards from the end of the string, making it intuitive to extract from the end
- No Argument Swapping: If
start > end, returns an empty string (predictable behavior) - Consistent Behavior: Predictable and intuitive behavior across all scenarios
- Similar to
Array.slice(): Works identically toArray.prototype.slice(), providing consistency across JavaScript APIs - Clamps Indices: Automatically handles out-of-bounds indices gracefully
- Immutable: Never modifies the original string, always returns a new string
- Exclusive End Index: The end index is exclusive (not included in the result), which is consistent with many programming languages
Why slice() is Recommended
slice() is the recommended method because:
- Modern Standard: Part of ECMAScript specification since ES3
- Intuitive: Behavior matches developer expectations
- Consistent: Works like
Array.slice(), reducing cognitive load - Flexible: Supports negative indices for convenient end-of-string operations
- Predictable: No surprising argument swapping or type coercion quirks
- Future-proof: Not deprecated, actively maintained and optimized
slice() Examples
Basic Usage
const str = "JavaScript"; // Length: 10 (characters)
// Index: 0123456789
// J a v a S c r i p t
// Example 1: Extract from start to end index (exclusive)
console.log(str.slice(0, 4)); // "Java"
// Explanation: Start at index 0 ('J'), end before index 4 (so includes indices 0,1,2,3)
// Result: "Java" (characters at positions 0, 1, 2, 3)
// Example 2: Extract middle portion
console.log(str.slice(4, 10)); // "Script"
// Explanation: Start at index 4 ('S'), end before index 10 (end of string)
// Result: "Script" (characters at positions 4, 5, 6, 7, 8, 9)
// Example 3: Extract from index to end (omitted endIndex)
console.log(str.slice(4)); // "Script"
// Explanation: When endIndex is omitted, it defaults to string.length
// Equivalent to: str.slice(4, str.length)
// Result: "Script" (from index 4 to end)
// Example 4: Extract single character
console.log(str.slice(0, 1)); // "J"
// Explanation: Start at 0, end before 1, so only index 0 is included
Negative Index
const str = "JavaScript"; // Length: 10
// Positive: 0123456789
// Negative: -10987654321
// Example 1: Negative start index only
console.log(str.slice(-6)); // "Script"
// Explanation: -6 becomes str.length + (-6) = 10 - 6 = 4
// Equivalent to: str.slice(4) which gives "Script"
// Example 2: Both indices negative
console.log(str.slice(-6, -1)); // "Scrip"
// Explanation:
// Start: -6 becomes 10 - 6 = 4
// End: -1 becomes 10 - 1 = 9
// Equivalent to: str.slice(4, 9) which gives "Scrip" (indices 4-8)
// Example 3: Positive start, negative end
console.log(str.slice(0, -1)); // "JavaScrip"
// Explanation:
// Start: 0
// End: -1 becomes 10 - 1 = 9
// Equivalent to: str.slice(0, 9) which gives "JavaScrip" (all except last char)
// Example 4: Negative start, positive end
console.log(str.slice(-4, 10)); // "ript"
// Explanation:
// Start: -4 becomes 10 - 4 = 6
// End: 10 (clamped to string.length)
// Result: "ript" (indices 6, 7, 8, 9)
// Example 5: Both negative, extracting middle
console.log(str.slice(-8, -3)); // "vaScr"
// Explanation:
// Start: -8 becomes 10 - 8 = 2
// End: -3 becomes 10 - 3 = 7
// Equivalent to: str.slice(2, 7) which gives "vaScr"
Type Coercion
const str = "JavaScript"; // Length: 10
// Example 1: String numbers are converted to numbers
console.log(str.slice("2", "5")); // "vaS"
// Explanation: "2" becomes 2, "5" becomes 5
// Equivalent to: str.slice(2, 5)
// Example 2: Non-numeric strings become NaN, treated as 0
console.log(str.slice("abc", 5)); // "JavaS"
// Explanation: "abc" becomes NaN, which is treated as 0
// Equivalent to: str.slice(0, 5)
// Example 3: null becomes 0
console.log(str.slice(null, 5)); // "JavaS"
// Explanation: null coerces to 0
// Example 4: undefined uses default behavior
console.log(str.slice(undefined, 5)); // "JavaS"
// Explanation: undefined for startIndex defaults to 0
console.log(str.slice(5, undefined)); // "Script"
// Explanation: undefined for endIndex defaults to string.length
// Example 5: Boolean values
console.log(str.slice(true, 5)); // "avaS"
// Explanation: true coerces to 1, false to 0
// Example 6: Floating point numbers are truncated
console.log(str.slice(2.7, 5.9)); // "vaS"
// Explanation: 2.7 becomes 2, 5.9 becomes 5 (floored)
// Equivalent to: str.slice(2, 5)
Edge Cases and Boundary Conditions
const str = "JavaScript"; // Length: 10
// Case 1: Empty range (start equals end)
console.log(str.slice(0, 0)); // "" (empty string)
console.log(str.slice(5, 5)); // "" (empty string)
// Explanation: When start >= end, result is always empty string
// Case 2: Start greater than end (no swapping!)
console.log(str.slice(5, 3)); // "" (empty string)
console.log(str.slice(10, 0)); // "" (empty string)
// Explanation: slice() does NOT swap arguments, returns empty string instead
// This is different from substring() which would swap!
// Case 3: Start beyond string length (end omitted)
console.log(str.slice(20)); // "" (empty string)
// Explanation: Start (20) > string.length (10), so clamped to 10
// Since end is omitted, it defaults to 10
// Result: str.slice(10, 10) β "" (empty string)
// Case 4: Start beyond string length, end within range
console.log(str.slice(20, 5)); // "" (empty string)
// Explanation: Start (20) > string.length (10), so clamped to 10
// End is 5 (within range)
// Result: str.slice(10, 5) β "" (empty string, because 10 > 5)
// Case 5: Negative beyond string length
console.log(str.slice(-20)); // "JavaScript" (entire string)
// Explanation: -20 becomes Math.max(0, 10 - 20) = Math.max(0, -10) = 0
// Equivalent to: str.slice(0) which gives entire string
// Case 6: End beyond string length
console.log(str.slice(0, 100)); // "JavaScript" (entire string)
// Explanation: End is clamped to string.length (10)
// Equivalent to: str.slice(0, 10)
// Case 7: Both indices beyond string length
console.log(str.slice(15, 20)); // "" (empty string)
console.log(str.slice(20, 25)); // "" (empty string)
// Explanation: Both indices are beyond string.length (10)
// Both are clamped to 10, so start(10) >= end(10), returns ""
// Case 8: Negative start, negative end, but start > end after conversion
console.log(str.slice(-3, -6)); // "" (empty string)
// Explanation:
// Start: -3 becomes 10 - 3 = 7
// End: -6 becomes 10 - 6 = 4
// Result: str.slice(7, 4) which is empty (7 > 4)
// Case 9: Positive start (not 0), negative end
console.log(str.slice(4, -3)); // "Scri" (from index 4 to -3, which is position 7)
// Explanation:
// Start: 4
// End: -3 becomes 10 - 3 = 7
// Result: str.slice(4, 7) which gives "Scri" (indices 4-6)
slice() with Negative Indices Explained (Visual Guide)
Understanding negative indices is crucial for effective string manipulation. Here's a detailed visual explanation:
const str = "JavaScript"; // Length: 10
// Visual representation with positive indices:
// Character: J a v a S c r i p t
// Index: 0 1 2 3 4 5 6 7 8 9
// Visual representation with negative indices:
// Character: J a v a S c r i p t
// Index: -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
// Formula: negativeIndex = string.length + negativeValue
// Example: -5 in "JavaScript" (length 10) = 10 + (-5) = 5
console.log(str.slice(-6)); // "Script"
// Step-by-step:
// 1. Convert -6: 10 + (-6) = 4
// 2. Start index: 4 (character 'S')
// 3. End index: 10 (end of string, default)
// 4. Extract: indices 4, 5, 6, 7, 8, 9 β "Script"
console.log(str.slice(-6, -1)); // "Scrip"
// Step-by-step:
// 1. Convert start -6: 10 + (-6) = 4
// 2. Convert end -1: 10 + (-1) = 9
// 3. Extract: indices 4, 5, 6, 7, 8 (exclusive of 9) β "Scrip"
console.log(str.slice(0, -6)); // "Java"
// Step-by-step:
// 1. Start index: 0 (character 'J')
// 2. Convert end -6: 10 + (-6) = 4
// 3. Extract: indices 0, 1, 2, 3 (exclusive of 4) β "Java"
Understanding Unicode and Multi-byte Characters
JavaScript strings are UTF-16 encoded, which means some characters (like emojis, certain Unicode characters) are represented using surrogate pairs (two 16-bit code units). This is important when working with slice(), substring(), and substr().
// Example 1: Basic ASCII characters (1 code unit each)
const ascii = "Hello";
console.log(ascii.slice(0, 2)); // "He" (works as expected)
// Example 2: Emojis (often 2 code units = 1 character)
const emoji = "Hello π World";
console.log(emoji.length); // 14 (not 12! π counts as 2, and there are 2 spaces)
console.log(emoji.slice(0, 8)); // "Hello π" (8 code units = 6 chars + emoji)
// Example 3: Splitting surrogate pairs (demonstrating the problem)
const unicode = "A\u{1D4AF}B"; // Mathematical script T
console.log(unicode.length); // 4 (A=1, π―=2, B=1)
console.log(unicode.slice(0, 2)); // "A" + first half of surrogate pair (split character - BAD!)
console.log(unicode.slice(0, 3)); // "Aπ―" (correct)
Important Notes on Unicode
slice(),substring(), andsubstr()all work with code units (UTF-16), not code points- Emojis and some Unicode characters use 2 code units (surrogate pairs)
- If you need to split by actual characters, consider using
Array.from(str)or the spread operator[...str] - For most practical purposes, the standard methods work fine with ASCII and common Unicode text
Understanding substring() / String.prototype.substring()
The String.prototype.substring() method (commonly called substring()) returns a part of the string between the start and end indexes, or to the end of the string. Unlike slice(), substring() has some quirky behaviors that can lead to unexpected results, making it less intuitive for many developers. substring() was one of the earliest string methods in JavaScript (since ES1) and has legacy behaviors that were kept for backward compatibility.
Syntax
string.substring(startIndex, endIndex)
Parameters
startIndex: The zero-based index at which to begin extraction. If undefined, negative, or NaN, it's treated as 0. If greater thanstring.length, it's treated asstring.length.endIndex(optional): The zero-based index before which to end extraction (exclusive). If omitted, defaults tostring.length. If undefined, negative, or NaN, it's treated as 0. If greater thanstring.length, it's treated asstring.length.
Key Features of substring()
- No Negative Indices: All negative values are treated as 0, losing the convenience of counting from the end
- Argument Swapping: If
start > end, the arguments are automatically swapped (this is the most confusing aspect) - Legacy Method: One of the oldest string methods (ES1), predating
slice()which was added in ES3 - NaN Handling: NaN values are treated as 0, which can mask programming errors
- Clamps to String Length: Values beyond
string.lengthare clamped tostring.length(not 0) - Immutable: Like
slice(), never modifies the original string - Exclusive End Index: The end index is exclusive (not included in result)
Why substring() Can Be Problematic
substring() has several behaviors that can lead to bugs:
- Argument Swapping: The automatic swapping can hide logic errors
- Negative Index Handling: Treating negatives as 0 loses information about intent
- Inconsistent with
slice(): Works differently from the more modernslice()method - NaN Coercion: Silently converts NaN to 0, which can hide bugs
Recommendation: Use slice() instead of substring() in new code to avoid these pitfalls.
substring() Examples
Basic Usage (Same as slice() in Simple Cases)
const str = "JavaScript"; // Length: 10
// Example 1: Basic extraction (works same as slice)
console.log(str.substring(0, 4)); // "Java"
// Explanation: Start at 0, end before 4 β indices 0,1,2,3 β "Java"
// Example 2: Middle portion
console.log(str.substring(4, 10)); // "Script"
// Explanation: Start at 4, end before 10 β indices 4-9 β "Script"
// Example 3: From index to end
console.log(str.substring(4)); // "Script"
// Explanation: When endIndex omitted, defaults to string.length
// Equivalent to: str.substring(4, 10)
// Example 4: Extract single character
console.log(str.substring(0, 1)); // "J"
// Explanation: Start at 0, end before 1, so only index 0 is included
Negative Indices (Always Treated as 0)
const str = "JavaScript"; // Length: 10
// Positive: 0123456789
// Negative: -10987654321
// Example 1: Negative start index only
console.log(str.substring(-6)); // "JavaScript"
// Step-by-step:
// 1. startIndex = -6 β treated as 0
// 2. endIndex omitted β defaults to 10
// 3. Extract: indices 0-9 β entire string "JavaScript"
// Equivalent to: str.substring(0)
// Comparison with slice():
console.log(str.slice(-6)); // "Script" (from index -6, which is position 4)
// slice() uses negative index as offset from end, substring() treats it as 0
// Example 2: Both indices negative
console.log(str.substring(-6, -1)); // "" (empty string)
// Step-by-step:
// 1. startIndex = -6 β treated as 0
// 2. endIndex = -1 β treated as 0
// 3. Check: 0 > 0? NO β No swap
// 4. Extract: indices from 0 to 0 (exclusive) β ""
// Comparison with slice():
console.log(str.slice(-6, -1)); // "Scrip" (characters from -6 to -1, which is indices 4-8)
// slice() correctly interprets both negative indices, substring() treats both as 0
// Example 3: Positive start, negative end
console.log(str.substring(0, -1)); // "" (empty string)
// Step-by-step:
// 1. startIndex = 0
// 2. endIndex = -1 β treated as 0
// 3. Check: 0 > 0? NO β No swap
// 4. Extract: indices from 0 to 0 (exclusive) β ""
// Equivalent to: str.substring(0, 0)
// Comparison with slice():
console.log(str.slice(0, -1)); // "JavaScrip" (from index 0 to -1, which is position 9)
// slice() correctly interprets negative end, substring() treats negative end as 0
// Example 4: Negative start, positive end
console.log(str.substring(-4, 10)); // "JavaScript"
// Step-by-step:
// 1. startIndex = -4 β treated as 0
// 2. endIndex = 10
// 3. Check: 0 > 10? NO β No swap
// 4. Extract: indices 0-9 β entire string "JavaScript"
// Equivalent to: str.substring(0, 10)
// Comparison with slice():
console.log(str.slice(-4, 10)); // "ript" (from index -4 which is position 6, to 10)
// slice() correctly interprets negative start (6), substring() treats negative start as 0
// Example 5: Both negative, extracting middle
console.log(str.substring(-8, -3)); // "" (empty string)
// Step-by-step:
// 1. startIndex = -8 β treated as 0
// 2. endIndex = -3 β treated as 0
// 3. Check: 0 > 0? NO β No swap
// 4. Extract: indices from 0 to 0 (exclusive) β ""
// Comparison with slice():
console.log(str.slice(-8, -3)); // "vaScr" (from -8 to -3, which is indices 2-6)
// slice() correctly extracts middle portion using negative indices, substring() treats both as 0
Type Coercion (Same for substring() and slice())
const str = "JavaScript"; // Length: 10
// Example 1: String numbers are converted to numbers
console.log(str.substring("2", "5")); // "vaS"
// Explanation: "2" becomes 2, "5" becomes 5
// Equivalent to: str.substring(2, 5)
// Comparison with slice():
console.log(str.slice("2", "5")); // "vaS" (same result)
// Example 2: Non-numeric strings become NaN, treated as 0
console.log(str.substring("abc", 5)); // "JavaS"
// Explanation: "abc" becomes NaN, which is treated as 0
// Equivalent to: str.substring(0, 5)
// Comparison with slice():
console.log(str.slice("abc", 5)); // "JavaS" (same result)
// Example 3: null becomes 0
console.log(str.substring(null, 5)); // "JavaS"
// Explanation: null coerces to 0
// Comparison with slice():
console.log(str.slice(null, 5)); // "JavaS" (same result)
// Example 4: undefined uses default behavior
console.log(str.substring(undefined, 5)); // "JavaS"
// Explanation: undefined for startIndex coerces to 0
console.log(str.substring(5, undefined)); // "Script"
// Explanation: undefined for endIndex defaults to string.length
// Comparison with slice():
console.log(str.slice(undefined, 5)); // "JavaS" (same result)
console.log(str.slice(5, undefined)); // "Script" (same result)
// Example 5: Boolean values
console.log(str.substring(true, 5)); // "avaS"
// Explanation: true coerces to 1, false becomes 0
// Comparison with slice():
console.log(str.slice(true, 5)); // "avaS" (same result)
// Example 6: Floating point numbers are truncated
console.log(str.substring(2.7, 5.9)); // "vaS"
// Explanation: 2.7 becomes 2, 5.9 becomes 5 (Math.floor applied)
// Equivalent to: str.substring(2, 5)
// Comparison with slice():
console.log(str.slice(2.7, 5.9)); // "vaS" (same result)
Edge Cases and Boundary Conditions
const str = "JavaScript"; // Length: 10
// Case 1: Empty range (start equals end)
console.log(str.substring(0, 0)); // "" (empty string)
console.log(str.substring(5, 5)); // "" (empty string)
// Explanation: When start equals end, result is always empty string
// Comparison with slice():
console.log(str.slice(0, 0)); // "" (same result)
console.log(str.slice(5, 5)); // "" (same result)
// Both methods behave the same when start equals end
// Case 2: Start greater than end (swapping occurs!)
console.log(str.substring(5, 3)); // "vaS"
// Step-by-step:
// 1. startIndex = 5, endIndex = 3
// 2. Check: 5 > 3? YES β SWAP arguments
// 3. Now: startIndex = 3, endIndex = 5
// 4. Extract: indices 3-4 β "vaS"
console.log(str.substring(10, 0)); // "JavaScript"
// Step-by-step:
// 1. startIndex = 10 (clamped to 10), endIndex = 0
// 2. Check: 10 > 0? YES β SWAP arguments
// 3. Now: startIndex = 0, endIndex = 10
// 4. Extract: entire string "JavaScript"
// Comparison with slice():
console.log(str.slice(5, 3)); // "" (empty string, no swapping)
console.log(str.slice(10, 0)); // "" (empty string, no swapping)
// slice() does NOT swap arguments, substring() DOES swap!
// Case 3: Start beyond string length (end omitted)
console.log(str.substring(20)); // "" (empty string)
// Explanation: 20 > 10, so clamped to 10
// For substring(20): startIndex=10, endIndex omitted β defaults to 10
// Result: str.substring(10, 10) β "" (empty string)
// Comparison with slice():
console.log(str.slice(20)); // "" (same result)
// Both behave the same when start is beyond string length and end is omitted
// Case 4: Start beyond string length, end within range
console.log(str.substring(20, 5)); // "Script"
// Step-by-step:
// 1. startIndex = 20 β clamped to 10
// 2. endIndex = 5
// 3. Check: 10 > 5? YES β SWAP arguments
// 4. Now: startIndex = 5, endIndex = 10
// 5. Extract: indices 5-9 β "Script"
// Equivalent to: str.substring(5, 10)
// Comparison with slice():
console.log(str.slice(20, 5)); // "" (empty string, no swapping)
// slice() does NOT swap, so returns "" when clamped start(10) > end(5)
// substring() SWAPS, so extracts from the valid range!
// Case 5: Negative beyond string length
console.log(str.substring(-20)); // "JavaScript" (entire string)
// Explanation: -20 is treated as 0
// Equivalent to: str.substring(0) which gives entire string
// Comparison with slice():
console.log(str.slice(-20)); // "JavaScript" (entire string)
// Both methods treat very negative indices as 0, giving entire string
// Case 6: End beyond string length
console.log(str.substring(0, 100)); // "JavaScript" (entire string)
// Explanation: endIndex 100 > 10, so clamped to 10
// Equivalent to: str.substring(0, 10) β entire string
// Comparison with slice():
console.log(str.slice(0, 100)); // "JavaScript" (same result)
// Both methods clamp end to string.length
// Case 7: Both indices beyond string length
console.log(str.substring(15, 20)); // "" (empty string)
console.log(str.substring(20, 25)); // "" (empty string)
// Step-by-step for substring(15, 20):
// 1. startIndex = 15 β clamped to 10
// 2. endIndex = 20 β clamped to 10
// 3. Check: 10 > 10? NO β No swap
// 4. Extract: indices from 10 to 10 (exclusive) β ""
// Step-by-step for substring(20, 25):
// 1. startIndex = 20 β clamped to 10
// 2. endIndex = 25 β clamped to 10
// 3. Check: 10 > 10? NO β No swap
// 4. Extract: indices from 10 to 10 (exclusive) β ""
// Comparison with slice():
console.log(str.slice(15, 20)); // "" (same result)
console.log(str.slice(20, 25)); // "" (same result)
// Both methods behave the same when both indices are beyond string length
// Case 8: Negative start, negative end, but start > end after conversion
console.log(str.substring(-3, -6)); // "" (empty string)
// Step-by-step:
// 1. startIndex = -3 β treated as 0
// 2. endIndex = -6 β treated as 0
// 3. Check: 0 > 0? NO β No swap
// 4. Extract: indices from 0 to 0 (exclusive) β ""
// Comparison with slice():
console.log(str.slice(-3, -6)); // "" (empty string)
// Explanation for slice():
// Start: -3 becomes 10 - 3 = 7
// End: -6 becomes 10 - 6 = 4
// Result: str.slice(7, 4) which is empty (7 > 4)
// Both return empty, but for different reasons:
// substring() treats both negatives as 0, slice() converts them and finds 7 > 4
// Case 9: Positive start (not 0), negative end
console.log(str.substring(4, -3)); // "Java"
// Step-by-step:
// 1. startIndex = 4
// 2. endIndex = -3 β treated as 0
// 3. Check: 4 > 0? YES β SWAP arguments
// 4. Now: startIndex = 0, endIndex = 4
// 5. Extract: indices 0-3 β "Java"
// Equivalent to: str.substring(0, 4)
// Comparison with slice():
console.log(str.slice(4, -3)); // "Scri" (from index 4 to -3, which is position 7)
// slice() correctly interprets negative end (7), substring() treats negative end as 0 and swaps
Argument Swapping Behavior (The Quirky Part!)
This is where substring() differs significantly from slice():
const str = "JavaScript"; // Length: 10
// Example 1: Arguments automatically swapped
console.log(str.substring(4, 0)); // "Java"
// Step-by-step:
// 1. startIndex = 4, endIndex = 0
// 2. Check: 4 > 0? YES β SWAP arguments
// 3. Now: startIndex = 0, endIndex = 4
// 4. Extract: indices 0-3 β "Java"
// Equivalent to: str.substring(0, 4)
// Comparison with slice() (doesn't swap!):
console.log(str.slice(4, 0)); // "" (empty string)
// This is why substring() can hide bugs - it "fixes" what might be an error
Understanding substr() / String.prototype.substr() (Deprecated)
The String.prototype.substr() method (commonly called substr()) returns a portion of the string, starting at the specified position and extending for a given number of characters. Important: substr() is deprecated and should not be used in new code.
Syntax
string.substr(startIndex, length)
Parameters
- startIndex: The index of the first character to include in the returned substring
- length (optional): The number of characters to extract. If omitted, extracts to the end of the string.
Key Features of substr()
- Second Parameter is Length: Unlike
slice()andsubstring(), the second parameter is the length, not the end index - Supports Negative Start: Negative start index counts from the end
- Deprecated: Should not be used in new code
- Inconsistent with Other Methods: Different parameter meaning causes confusion
Why substr() is Deprecated
substr() has been deprecated because:
- Inconsistent Parameter Meaning: Second parameter is length (not end index like
slice()andsubstring()) - Confusing for Developers: Different from the more commonly used
slice()andsubstring()methods - Not Part of ECMAScript Standard: Though widely supported, it's not part of the official JavaScript specification
- Can Be Replaced: All use cases can be handled with
slice()instead
Recommendation: Use slice() instead of substr() in all cases.
substr() Examples
const str = "JavaScript"; // Length: 10
// Basic usage (NOTE: second param is LENGTH, not end index!)
console.log(str.substr(0, 4)); // "Java" (4 characters from index 0)
// Comparison with slice():
console.log(str.slice(0, 4)); // "Java" (same result - from index 0 to 4, exclusive)
// substr(0, 4) uses length 4, slice(0, 4) uses end index 4 - same result in this case
console.log(str.substr(4, 6)); // "Script" (6 characters from index 4)
// Comparison with slice():
console.log(str.slice(4, 4 + 6)); // "Script" (from index 4 to 10, exclusive)
// substr(4, 6) uses length 6, slice(4, 10) uses end index 10 - same result
console.log(str.substr(4)); // "Script" (to end - length omitted)
// Comparison with slice():
console.log(str.slice(4)); // "Script" (from index 4 to end - same result)
// Negative start index
console.log(str.substr(-6)); // "Script" (last 6 characters)
// Comparison with slice():
console.log(str.slice(-6)); // "Script" (from index -6 to end - same result)
console.log(str.substr(-6, -1)); // "" (empty string - negative length treated as 0)
// Comparison with slice():
console.log(str.slice(-6, -1)); // "Scrip" (from index -6 to -1, exclusive)
// substr(-6, -1) uses negative length (treated as 0), slice(-6, -1) uses negative end index
// Edge cases
console.log(str.substr(0, 0)); // "" (length 0)
// Comparison with slice():
console.log(str.slice(0, 0)); // "" (from index 0 to 0, exclusive - same result)
console.log(str.substr(0, 100)); // "JavaScript" (entire string, length capped)
// Comparison with slice():
console.log(str.slice(0, 100)); // "JavaScript" (entire string, end index clamped - same result)
console.log(str.substr(20)); // "" (start beyond string length)
// Comparison with slice():
console.log(str.slice(20)); // "" (start beyond string length - same result)
// Negative start with negative length
console.log(str.substr(-4, -1)); // "" (empty string - negative length treated as 0)
// Comparison with slice():
console.log(str.slice(-4, -1)); // "rip" (from index -4 to -1, exclusive)
// substr(-4, -1) uses negative length (treated as 0), slice(-4, -1) uses negative end index
Side-by-Side Comparison
Let's compare all three methods with the same inputs to see how they differ:
const str = "JavaScript"; // Length: 10
console.log("Original string:", str);
console.log("String length:", str.length);
// Example 1: Basic extraction
console.log("\n=== Example 1: Basic extraction (0, 4) ===");
console.log("slice(0, 4):", str.slice(0, 4)); // "Java"
console.log("substring(0, 4):", str.substring(0, 4)); // "Java"
console.log("substr(0, 4):", str.substr(0, 4)); // "Java" (4 characters)
// Example 2: Negative start
console.log("\n=== Example 2: Negative start (-6) ===");
console.log("slice(-6):", str.slice(-6)); // "Script"
console.log("substring(-6):", str.substring(-6)); // "JavaScript" (treated as 0)
console.log("substr(-6):", str.substr(-6)); // "Script"
// Example 3: Start > End
console.log("\n=== Example 3: Start > End (5, 3) ===");
console.log("slice(5, 3):", str.slice(5, 3)); // "" (empty)
console.log("substring(5, 3):", str.substring(5, 3)); // "vaS" (swaps to substring(3, 5))
console.log("substr(5, 3):", str.substr(5, 3)); // "cri" (5th index, length 3)
// Example 4: Negative end
console.log("\n=== Example 4: Negative end (0, -6) ===");
console.log("slice(0, -6):", str.slice(0, -6)); // "Java"
console.log("substring(0, -6):", str.substring(0, -6)); // "" (negative treated as 0, becomes substring(0, 0))
console.log("substr(0, -6):", str.substr(0, -6)); // "" (negative length treated as 0)
// Example 5: Omitted end parameter
console.log("\n=== Example 5: Start only (4) ===");
console.log("slice(4):", str.slice(4)); // "Script"
console.log("substring(4):", str.substring(4)); // "Script"
console.log("substr(4):", str.substr(4)); // "Script"
// Example 6: Positive start with negative end (substring swaps!)
console.log("\n=== Example 6: Positive start, negative end (4, -3) ===");
console.log("slice(4, -3):", str.slice(4, -3)); // "Scr" (from index 4 to -3, which is position 7)
console.log("substring(4, -3):", str.substring(4, -3)); // "Java" (negative treated as 0, swaps to substring(0, 4))
console.log("substr(4, -3):", str.substr(4, -3)); // "" (negative length treated as 0)
Real-World Use Cases
Use Case 1: Extracting File Extensions
function getFileExtension(filename) {
// Using slice() - recommended approach
const lastDot = filename.lastIndexOf('.');
if (lastDot === -1) return '';
return filename.slice(lastDot + 1);
}
console.log(getFileExtension('document.pdf')); // "pdf"
console.log(getFileExtension('image.jpg')); // "jpg"
console.log(getFileExtension('archive.tar.gz')); // "gz"
console.log(getFileExtension('noextension')); // ""
Use Case 2: Truncating Text with Ellipsis
function truncate(text, maxLength) {
if (text.length <= maxLength) return text;
// Using slice() to get first maxLength characters
return text.slice(0, maxLength - 3) + '...';
}
console.log(truncate('This is a long text', 10)); // "This is..."
console.log(truncate('Short', 10)); // "Short"
Use Case 3: Extracting Domain from URL
function extractDomain(url) {
// Remove protocol
let domain = url.replace(/^https?:\/\//, '');
// Extract domain part (before first slash)
const slashIndex = domain.indexOf('/');
if (slashIndex !== -1) {
domain = domain.slice(0, slashIndex);
}
return domain;
}
console.log(extractDomain('https://example.com/path')); // "example.com"
console.log(extractDomain('http://subdomain.example.com')); // "subdomain.example.com"
Use Case 4: Getting Last N Characters
function getLastNChars(str, n) {
// Using slice() with negative index - clean and readable
return str.slice(-n);
}
console.log(getLastNChars('JavaScript', 4)); // "ript"
console.log(getLastNChars('Hello World', 5)); // "World"
// Alternative with substring (less intuitive):
function getLastNCharsSubstring(str, n) {
return str.substring(str.length - n);
}
Use Case 5: Masking Sensitive Information
function maskEmail(email) {
const [localPart, domain] = email.split('@');
if (localPart.length <= 2) {
return email; // Too short to mask
}
// Show first 2 and last character, mask the rest
const visible = localPart.slice(0, 2) + localPart.slice(-1);
const masked = localPart.slice(2, -1).replace(/./g, '*');
return visible.slice(0, 2) + masked + visible.slice(-1) + '@' + domain;
}
console.log(maskEmail('john.doe@example.com')); // "jo***e@example.com"
console.log(maskEmail('user@example.com')); // "us*r@example.com"
// More advanced masking functions
function maskCreditCard(cardNumber) {
// Show only last 4 digits
if (cardNumber.length < 4) return cardNumber;
const last4 = cardNumber.slice(-4);
const masked = '*'.repeat(Math.max(0, cardNumber.length - 4));
return masked + last4;
}
console.log(maskCreditCard('1234567890123456')); // "************3456"
function maskPhoneNumber(phone) {
// Show last 4 digits, mask the rest
if (phone.length < 4) return phone;
const last4 = phone.slice(-4);
const masked = phone.slice(0, -4).replace(/\d/g, '*');
return masked + last4;
}
console.log(maskPhoneNumber('555-123-4567')); // "***-***-4567"
Use Case 6: Parsing and Formatting Dates
// Extracting date components from ISO format
function parseISODate(isoString) {
// Format: "2024-01-15T10:30:00Z"
const datePart = isoString.slice(0, 10); // "2024-01-15"
const timePart = isoString.slice(11, 19); // "10:30:00"
const year = datePart.slice(0, 4); // "2024"
const month = datePart.slice(5, 7); // "01"
const day = datePart.slice(8, 10); // "15"
return {
year: parseInt(year, 10),
month: parseInt(month, 10),
day: parseInt(day, 10),
date: datePart,
time: timePart
};
}
console.log(parseISODate('2024-01-15T10:30:00Z'));
// { year: 2024, month: 1, day: 15, date: '2024-01-15', time: '10:30:00' }
// Formatting dates
function formatDate(dateString) {
// Input: "20240115" β Output: "2024-01-15"
const year = dateString.slice(0, 4);
const month = dateString.slice(4, 6);
const day = dateString.slice(6, 8);
return `${year}-${month}-${day}`;
}
Use Case 7: Text Processing and Word Extraction
// Extract first N words from text
function getFirstWords(text, count) {
const words = text.split(/\s+/);
if (words.length <= count) return text;
return words.slice(0, count).join(' ') + '...';
}
console.log(getFirstWords('This is a long sentence with many words', 4));
// "This is a long..."
// Extract last N words
function getLastWords(text, count) {
const words = text.split(/\s+/);
if (words.length <= count) return text;
return words.slice(-count).join(' ');
}
console.log(getLastWords('This is a long sentence with many words', 3));
// "with many words"
// Extract sentences
function getFirstSentence(text) {
const sentenceEnd = text.search(/[.!?]/);
if (sentenceEnd === -1) return text;
return text.slice(0, sentenceEnd + 1);
}
Use Case 8: URL and Path Manipulation
// Extract path segments
function getPathSegments(url) {
const pathStart = url.indexOf('/', url.indexOf('://') + 3);
if (pathStart === -1) return [];
const path = url.slice(pathStart + 1);
return path.split('/').filter(Boolean);
}
console.log(getPathSegments('https://example.com/users/123/posts'));
// ["users", "123", "posts"]
// Get parent directory from file path
function getParentPath(filePath) {
const lastSlash = filePath.lastIndexOf('/');
if (lastSlash === -1) return '.';
return filePath.slice(0, lastSlash) || '/';
}
console.log(getParentPath('/users/john/documents/file.txt'));
// "/users/john/documents"
// Extract query parameters
function getQueryString(url) {
const queryStart = url.indexOf('?');
if (queryStart === -1) return '';
const hashStart = url.indexOf('#');
if (hashStart !== -1) {
return url.slice(queryStart + 1, hashStart);
}
return url.slice(queryStart + 1);
}
console.log(getQueryString('https://example.com/search?q=javascript&page=1'));
// "q=javascript&page=1"
console.log(getQueryString('https://example.com/search?q=javascript#results'));
// "q=javascript"
console.log(getQueryString('https://example.com/search'));
// ""
Use Case 9: String Validation and Sanitization
// Validate and extract username from email
function extractUsername(email) {
const atIndex = email.indexOf('@');
if (atIndex === -1 || atIndex === 0) return null;
const username = email.slice(0, atIndex);
// Validate username (alphanumeric, dots, underscores)
if (!/^[a-zA-Z0-9._]+$/.test(username)) return null;
return username;
}
console.log(extractUsername('john.doe@example.com'));
// "john.doe"
console.log(extractUsername('user_name123@domain.com'));
// "user_name123"
console.log(extractUsername('invalid@email'));
// null
console.log(extractUsername('@example.com'));
// null
console.log(extractUsername('user@name@example.com'));
// null
// Remove HTML tags (simple version)
function stripHTML(html) {
let text = html;
while (text.includes('<')) {
const tagStart = text.indexOf('<');
const tagEnd = text.indexOf('>', tagStart);
if (tagEnd === -1) break;
text = text.slice(0, tagStart) + text.slice(tagEnd + 1);
}
return text;
}
console.log(stripHTML('<p>Hello <strong>world</strong>!</p>'));
// "Hello world!"
console.log(stripHTML('<div>Text with <span>nested</span> tags</div>'));
// "Text with nested tags"
console.log(stripHTML('Plain text without tags'));
// "Plain text without tags"
Common Pitfalls and Mistakes
Pitfall 1: Confusing substring() with slice()
const str = "Hello World";
// This works as expected
console.log(str.slice(0, 5)); // "Hello"
// But this behaves differently!
console.log(str.substring(5, 0)); // "Hello" (swaps to substring(0, 5))
console.log(str.slice(5, 0)); // "" (empty string)
// Solution: Use slice() consistently to avoid confusion
Pitfall 2: Using Negative Indices with substring()
const str = "JavaScript"; // Length: 10
// This doesn't work as you might expect
console.log(str.substring(-3)); // "JavaScript" (entire string, not last 3)
console.log(str.substring(-3, -1)); // "" (both become 0, so substring(0, 0))
// Solution: Use slice() when you need negative indices
console.log(str.slice(-3)); // "ipt" (last 3 characters)
console.log(str.slice(-3, -1)); // "ip" (from -3 to -1)
Pitfall 3: Using substr() with End Index Instead ofΒ Length
const str = "JavaScript";
// Wrong: Thinking substr works like slice/substring
console.log(str.substr(0, 4)); // "Java" (4 characters, not up to index 4)
console.log(str.slice(0, 4)); // "Java" (up to index 4, exclusive)
// This difference matters!
console.log(str.substr(4, 6)); // "Script" (6 characters from index 4)
console.log(str.slice(4, 6)); // "Sc" (from index 4 to 6, exclusive)
// Solution: Remember substr's second param is LENGTH, not end index
// Better: Use slice() instead of substr()
Best Practices
1. Prefer slice() Over substring() and substr()
// β
GOOD: Use slice() for consistency and modern JavaScript practices
const username = email.slice(0, email.indexOf('@'));
const domain = email.slice(email.indexOf('@') + 1);
// β AVOID: Using substring() or substr() in new code
const usernameOld = email.substring(0, email.indexOf('@'));
const domainOld = email.substr(email.indexOf('@') + 1);
2. Use Negative Indices with slice() for Cleaner Code
// β
GOOD: Using negative indices with slice()
const lastChar = str.slice(-1); // Last character
const lastThree = str.slice(-3); // Last 3 characters
const allButLast = str.slice(0, -1); // All except last character
// β AVOID: Calculating indices manually
const lastCharOld = str.slice(str.length - 1); // Less readable, manual calculation
const lastThreeOld = str.slice(str.length - 3); // Less readable, manual calculation
const allButLastOld = str.slice(0, str.length - 1); // Less readable, manual calculation
3. Always Specify Both Parameters When Clarity Matters
// β
GOOD: Explicit parameters for clarity
const substring = str.slice(startIndex, endIndex);
// β οΈ ACCEPTABLE: Omitting end parameter when extracting to end
const rest = str.slice(startIndex);
4. Be Aware of Index Boundaries
// β
GOOD: Understand that end index is exclusive
const str = "Hello";
console.log(str.slice(0, 5)); // "Hello" (indices 0,1,2,3,4)
console.log(str.slice(0, str.length)); // "Hello" (all characters)
// Remember: end index is NOT inclusive!
console.log(str.slice(0, 4)); // "Hell" (not "Hello")
5. Use Consistent Method Across Your Codebase
// β
GOOD: Consistent use of slice() throughout
function processString(str) {
const first = str.slice(0, 5);
const middle = str.slice(5, -5);
const last = str.slice(-5);
return { first, middle, last };
}
// β AVOID: Mixing different methods inconsistently
function processStringInconsistent(str) {
const first = str.substring(0, 5); // substring
const middle = str.slice(5, -5); // slice
const last = str.substr(-5); // substr (deprecated!)
return { first, middle, last };
}
Performance Considerations
While all three methods have similar performance characteristics, slice() is generally preferred because:
- Modern Standard: Part of ECMAScript standard
- Consistent Behavior: Predictable and intuitive
- Array Compatibility: Works similarly to
Array.slice(), reducing cognitive load - Future-Proof: Not deprecated, actively maintained
// Performance is similar for all methods
const str = "a".repeat(1000000);
console.time('slice');
for (let i = 0; i < 10000; i++) {
str.slice(100, 200);
}
console.timeEnd('slice');
console.time('substring');
for (let i = 0; i < 10000; i++) {
str.substring(100, 200);
}
console.timeEnd('substring');
console.time('substr');
for (let i = 0; i < 10000; i++) {
str.substr(100, 100);
}
console.timeEnd('substr');
// In most cases, performance difference is negligible
// Choose based on functionality, not performance
Browser Compatibility
All three methods are widely supported:
slice(): Supported in all modern browsers (IE4+), part of ECMAScript 3 standardsubstring(): Supported in all modern browsers (IE3+), part of ECMAScript 1 standardsubstr(): Supported but deprecated (IE3+, but should not be used), not part of ECMAScript standard
For modern JavaScript development, slice() is the recommended choice as it's part of the ECMAScript standard and has the most intuitive behavior.
Further Learning Resources
Mastering JavaScript string methods is just one piece of becoming a proficient JavaScript developer. If you're looking to deepen your JavaScript knowledge and build real-world applications, there are excellent learning resources available.
For developers who prefer interactive, text-based learning over video tutorials, the Complete JavaScript Course: Build a Real World App from Scratch on Educative offers a video-free, hands-on approach with embedded code editors and immediate feedback. If you prefer video-based learning from industry experts, the Programming with JavaScript course by Meta on Coursera provides structured video lectures as part of Meta's Front-End Developer Professional Certificate program.
Conclusion
Understanding the differences between String.prototype.slice(), String.prototype.substring(), and String.prototype.substr() (commonly used as slice(), substring(), and substr()) is crucial for writing clean, maintainable JavaScript code. Throughout this comprehensive guide, we've explored the intricate behaviors of each method, their use cases, pitfalls, and best practices.
Quick Comparison Table
Comparison of String.prototype.slice(), String.prototype.substring(), and String.prototype.substr() (commonly used as slice(), substring(), and substr()):
| Feature | slice() / String.prototype.slice() | substring() / String.prototype.substring() | substr() / String.prototype.substr() |
|---|---|---|---|
| Syntax | slice(start, end) | substring(start, end) | substr(start, length) |
| Second Parameter | End index (exclusive) | End index (exclusive) | Length of substring |
| Negative Index | Supported (counts from end) | Not supported (treated as 0) | Supported (start only) |
| Swaps Arguments | No | Yes (if start > end) | No |
| Start > End | Returns empty string | Swaps arguments | N/A (second param is length, not end index) |
| ECMAScript Version | ES3+ (modern standard) | ES1 (legacy) | Not part of standard |
| Specification | Part of official JavaScript spec | Part of official JavaScript spec | Not part of ECMAScript standard |
| Array API Consistency | Consistent with Array.slice() | N/A | N/A |
| NaN Handling | NaN treated as 0 | NaN treated as 0 | NaN treated as 0 |
| Type Coercion | Same for all: coerces non-numbers to numbers (NaN/undefined/null β 0) | Same for all: coerces non-numbers to numbers (NaN/undefined/null β 0) | Same for all: coerces non-numbers to numbers (NaN/undefined/null β 0) |
| Status | Recommended β | Legacy β οΈ | Deprecated β |