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 below0, it is clamped to0. - 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 paststr.length, it is clamped tostr.length. - Return value — a new string containing the extracted characters. If, after resolving,
startis greater than or equal toend, 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
strto a string primitive (if it were a String object, though in practice you’re almost always already working with a primitive). - 2. Let
lenbestr.length. - 3. Resolve
start: if negative, computemax(len + start, 0); otherwise computemin(start, len). - 4. Resolve
end: if omitted, uselen. If negative, computemax(len + end, 0); otherwise computemin(end, len). - 5. If the resolved
startis greater than or equal to the resolvedend, return"". - 6. Otherwise, build and return a new string containing the code units from the resolved
startup to (but not including) the resolvedend.
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()oversubstr()—substr()is marked as a legacy feature in the specification and may be removed from future JavaScript engines. - Prefer
slice()oversubstring()when you need negative-index support (e.g. “the last 5 characters”), sincesubstring()ignores negatives entirely. - Remember that
endis 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 likestr.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 likeindexOf()andlastIndexOf()to extract meaningful substrings (extensions, domains, query strings) rather than hardcoding numeric positions.
Practice Exercises
- 1. Given
const url = "https://example.com/path";, useslice()together withindexOf()to extract just"example.com/path"(everything after"https://"). - 2. Write a function
lastChars(str, n)that returns the lastncharacters ofstrusing a single call toslice()with a negative index. Test it withlastChars("JavaScript", 6)and confirm it prints"Script". - 3. Given
const sentence = "The quick brown fox";, predict the output ofsentence.slice(4, -10)by hand (resolve the negative index first), then check your reasoning against what you now know about howslice()clamps and resolves indices.
Summary
slice(start, end)extracts a substring fromstart(inclusive) toend(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
startis not less than the resolvedend,slice()returns an empty string. substring()treats negative arguments as0and 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.
