JavaScript String Slicing

String slicing is the act of pulling out a piece of a string — a substring — based on numeric positions, without changing the original string. In JavaScript this is done mainly with the slice() method. It is one of the most-used string tools in the language, showing up everywhere from parsing file extensions to trimming text for display. Understanding exactly how indices are counted, and what happens when you pass negative numbers or out-of-range values, will save you from a lot of subtle off-by-one bugs.

Overview: How String Slicing Works

Strings in JavaScript are primitive values, and primitives are immutable — once a string is created, none of its characters can be changed in place. Every string method that appears to “modify” a string, including slice(), actually returns a brand-new string and leaves the original completely untouched.

Internally, a JavaScript string is treated as an ordered sequence of UTF-16 code units, indexed starting at 0. The character at index 0 is the first character, the character at index 1 is the second, and so on, up to length - 1. When you call str.slice(start, end), the engine does not loop through characters one by one in a way you can observe — it resolves start and end into two clamped positions and produces the characters in that half-open range: including start, excluding end.

Modern JavaScript engines like V8 (used in Chrome and Node.js) also optimize this internally. Instead of always copying characters into a new buffer, V8 can create a lightweight SlicedString object that simply points back into the original string’s memory along with an offset and length, only materializing a real copy later if needed. This is an implementation detail you cannot observe from your code, but it explains why slicing large strings is cheap — the engine is not necessarily duplicating megabytes of text every time you call slice().

JavaScript has three related methods for extracting substrings: slice(), substring(), and the older, deprecated substr(). They look similar but behave differently with negative or reversed arguments, which is a frequent source of bugs. This lesson focuses on slice() as the modern, recommended choice, and explains how the other two differ.

Syntax

str.slice(start)
str.slice(start, end)
  • str — the string to extract from (the string the method is called on).
  • start — the index to begin extraction at (inclusive). If negative, it is treated as str.length + start, i.e. counted from the end of the string. If omitted or if it resolves below 0, it is clamped to 0.
  • end (optional) — the index to stop extraction at (exclusive — this character is not included). If negative, it is treated as str.length + end. If omitted, or if it resolves past str.length, it is clamped to str.length.
  • Return value — a new string containing the extracted characters. If, after resolving, start is greater than or equal to end, an empty string "" is returned.
Method Negative indices start > end behavior Status
slice(start, end) Counted from the end of the string Returns "" Recommended
substring(start, end) Treated as 0 Automatically swaps the two arguments Fine, but less flexible
substr(start, length) start counted from the end; second argument is a length, not an index N/A (different signature) Deprecated — avoid

Examples

Example 1: Basic slicing with positive and negative indices

const str = "JavaScript Programming";

console.log(str.slice(0, 10));
console.log(str.slice(11));
console.log(str.slice(-11));

Output:

JavaScript
Programming
Programming

The first call extracts indices 0 through 9 (index 10 is excluded), giving "JavaScript". The second call omits end, so it runs to the end of the string, giving "Programming". The third call uses -11, which is resolved as 23 - 11 = 12… actually since the string is 22 characters long, it resolves to 22 - 11 = 11, the exact same starting position as 11 — a handy way to grab “the last N characters” without doing the length arithmetic yourself.

Example 2: Clamping and comparing slice() with substring()

const text = "Hello, World!";

console.log(text.slice(-6, -1));
console.log(text.substring(-6, 5));
console.log(text.slice(7, 100));

Output:

World
Hello
World!

In the first line, both arguments are negative: -6 becomes index 7 and -1 becomes index 12, extracting "World". In the second line, substring() does not understand negative numbers the way slice() does — it simply treats any negative argument as 0, so it becomes substring(0, 5), giving "Hello" instead of the suffix you might have expected. The third line shows clamping: asking for an end of 100 on a 13-character string is automatically capped at the string’s actual length, so it safely returns everything from index 7 onward with no error.

Example 3: Real-world use — extracting a file extension, and slice() vs substring() with swapped arguments

const filename = "report-final.docx";
const extension = filename.slice(filename.lastIndexOf(".") + 1);
console.log(extension);

const a = "Sequence";
console.log(a.slice(5, 2));
console.log(a.substring(5, 2));

Output:

docx

que

lastIndexOf(".") finds the position of the final dot, and slicing from one past that position isolates the extension — a very common pattern for parsing filenames, URLs, or paths. The second part shows a key divergence: when start is greater than end, slice(5, 2) simply returns an empty string, while substring(5, 2) silently swaps the arguments to substring(2, 5) and returns "que". Relying on that auto-swap behavior can hide bugs, since your code keeps “working” even when the arguments are in the wrong order.

How It Works Step by Step

When the engine evaluates str.slice(start, end), it effectively performs these steps:

  • 1. Convert str to a string primitive (if it were a String object, though in practice you’re almost always already working with a primitive).
  • 2. Let len be str.length.
  • 3. Resolve start: if negative, compute max(len + start, 0); otherwise compute min(start, len).
  • 4. Resolve end: if omitted, use len. If negative, compute max(len + end, 0); otherwise compute min(end, len).
  • 5. If the resolved start is greater than or equal to the resolved end, return "".
  • 6. Otherwise, build and return a new string containing the code units from the resolved start up to (but not including) the resolved end.

Notice that every branch clamps values into the valid range [0, len] — this is why slice() never throws an error for out-of-range numbers, unlike, say, accessing an array index that doesn’t exist and getting undefined. Slicing a string always yields a string, even if that string is empty.

Common Mistakes

Mistake 1: Assuming slice() mutates the original string

const message = "hello world";
const greeting = message.slice(0, 5);

console.log(message);
console.log(greeting);

Output:

hello world
hello

Because strings are immutable, calling message.slice(0, 5) never changes message itself — it only returns a new string. Forgetting to capture the return value in a variable (writing message.slice(0, 5); alone, with no assignment) is a common beginner mistake: the extracted substring is simply discarded, and the original variable is left unchanged.

Mistake 2: Treating the second argument as a length instead of an end index

const s = "abcdefgh";

console.log(s.slice(2, 3));
console.log(s.slice(2, 2 + 3));

Output:

c
cde

It’s easy to confuse slice(start, end) with the deprecated substr(start, length), where the second argument means “how many characters to take.” With slice(), the second argument is an index, not a count. If you actually want “three characters starting at index 2,” you must add the desired length to the start: s.slice(2, 2 + 3), which correctly returns "cde".

Best Practices

  • Prefer slice() over substr()substr() is marked as a legacy feature in the specification and may be removed from future JavaScript engines.
  • Prefer slice() over substring() when you need negative-index support (e.g. “the last 5 characters”), since substring() ignores negatives entirely.
  • Remember that end is exclusive — the character at that index is not part of the result — so count carefully when computing exact substring lengths.
  • Use negative indices (str.slice(-n)) instead of manual arithmetic like str.slice(str.length - n); it’s shorter and less error-prone.
  • Always assign the result of slice() to a variable (or use it directly); calling it and discarding the return value does nothing useful, since the original string never changes.
  • Combine slice() with methods like indexOf() and lastIndexOf() to extract meaningful substrings (extensions, domains, query strings) rather than hardcoding numeric positions.

Practice Exercises

  • 1. Given const url = "https://example.com/path";, use slice() together with indexOf() to extract just "example.com/path" (everything after "https://").
  • 2. Write a function lastChars(str, n) that returns the last n characters of str using a single call to slice() with a negative index. Test it with lastChars("JavaScript", 6) and confirm it prints "Script".
  • 3. Given const sentence = "The quick brown fox";, predict the output of sentence.slice(4, -10) by hand (resolve the negative index first), then check your reasoning against what you now know about how slice() clamps and resolves indices.

Summary

  • slice(start, end) extracts a substring from start (inclusive) to end (exclusive) without modifying the original string.
  • Negative indices count backward from the end of the string: str.length + index.
  • Out-of-range values are clamped into [0, length] rather than throwing errors.
  • If the resolved start is not less than the resolved end, slice() returns an empty string.
  • substring() treats negative arguments as 0 and auto-swaps reversed arguments, which can silently mask bugs.
  • substr() is deprecated and should be avoided in new code.
  • Strings are immutable, so slicing always returns a new string and never changes the original.