Published on

slice() vs substring() vs substr(): Complete JavaScript String Methods Comparison

Authors
slice() vs substring() vs substr(): Complete JavaScript String Methods Comparison Infographics
Table of Contents

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 to string.length + startIndex. If undefined, defaults to 0. If greater than string.length, it's clamped to string.length.
  • endIndex (optional): The zero-based index before which to end extraction (exclusive). Can be negative (counts from end). If negative, it's converted to string.length + endIndex. If omitted or undefined, defaults to string.length. If greater than string.length, it's clamped to string.length.

Key Features of slice()

  1. Supports Negative Indices: Negative indices count backwards from the end of the string, making it intuitive to extract from the end
  2. No Argument Swapping: If start > end, returns an empty string (predictable behavior)
  3. Consistent Behavior: Predictable and intuitive behavior across all scenarios
  4. Similar to Array.slice(): Works identically to Array.prototype.slice(), providing consistency across JavaScript APIs
  5. Clamps Indices: Automatically handles out-of-bounds indices gracefully
  6. Immutable: Never modifies the original string, always returns a new string
  7. Exclusive End Index: The end index is exclusive (not included in the result), which is consistent with many programming languages

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(), and substr() 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 than string.length, it's treated as string.length.
  • endIndex (optional): The zero-based index before which to end extraction (exclusive). If omitted, defaults to string.length. If undefined, negative, or NaN, it's treated as 0. If greater than string.length, it's treated as string.length.

Key Features of substring()

  1. No Negative Indices: All negative values are treated as 0, losing the convenience of counting from the end
  2. Argument Swapping: If start > end, the arguments are automatically swapped (this is the most confusing aspect)
  3. Legacy Method: One of the oldest string methods (ES1), predating slice() which was added in ES3
  4. NaN Handling: NaN values are treated as 0, which can mask programming errors
  5. Clamps to String Length: Values beyond string.length are clamped to string.length (not 0)
  6. Immutable: Like slice(), never modifies the original string
  7. 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 modern slice() 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()

  1. Second Parameter is Length: Unlike slice() and substring(), the second parameter is the length, not the end index
  2. Supports Negative Start: Negative start index counts from the end
  3. Deprecated: Should not be used in new code
  4. 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() and substring())
  • Confusing for Developers: Different from the more commonly used slice() and substring() 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:

  1. Modern Standard: Part of ECMAScript standard
  2. Consistent Behavior: Predictable and intuitive
  3. Array Compatibility: Works similarly to Array.slice(), reducing cognitive load
  4. 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 standard
  • substring(): Supported in all modern browsers (IE3+), part of ECMAScript 1 standard
  • substr(): 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()):

Featureslice() / String.prototype.slice()substring() / String.prototype.substring()substr() / String.prototype.substr()
Syntaxslice(start, end)substring(start, end)substr(start, length)
Second ParameterEnd index (exclusive)End index (exclusive)Length of substring
Negative IndexSupported (counts from end)Not supported (treated as 0)Supported (start only)
Swaps ArgumentsNoYes (if start > end)No
Start > EndReturns empty stringSwaps argumentsN/A (second param is length, not end index)
ECMAScript VersionES3+ (modern standard)ES1 (legacy)Not part of standard
SpecificationPart of official JavaScript specPart of official JavaScript specNot part of ECMAScript standard
Array API ConsistencyConsistent with Array.slice()N/AN/A
NaN HandlingNaN treated as 0NaN treated as 0NaN treated as 0
Type CoercionSame 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)
StatusRecommended βœ…Legacy ⚠️Deprecated ❌