String.prototype.indexOf()

Use it when you need the position — to slice around it. When you only want to know whether the text is there, includes says so without the -1 ceremony.

String methodES1 (1997)Live demo
Common call
s.indexOf('/')
Returns
a number, or -1 when absent
Replaces
a manual character-by-character scan
Watch out
0 is falsy — never test the result for truthiness
string.indexOf(searchString[, position])
→ number

Demo

Live evaluation
Try:
Inputs
sstringthe string to search
searchstringtext to look for
Output
'hello'.indexOf('l')
2

Only the FIRST occurrence is reported — 'hello' has two l's and the answer is 2, not a list. The second case is the one that causes bugs: a match at the very start returns 0, which is falsy, so `if (s.indexOf(x))` treats a successful match at position 0 as a failure. That is why the correct comparison is against -1 explicitly, or better, why includes exists. An empty search string returns 0 for the same reason includes('') is true — it occurs trivially at the beginning.

Parameters

NameTypeRequiredDescription
searchStringstringyesText to find. Unlike includes, a RegExp is not rejected — it is stringified, which almost never does what you want.
positionnumberno (0)Where to start searching. For lastIndexOf this instead means the highest index the match may START at, searching backwards.

Return value

number — The index of the first occurrence at or after position, or -1 if there is none. Zero is a valid result, which is what makes truthiness checks wrong.

Common patterns

Split around the first delimiter
Where split with a limit would lose the tail.
const i = s.indexOf(':');
const [k, v] = [s.slice(0, i), s.slice(i + 1)];
File extension
lastIndexOf, because names may contain several dots.
const ext = name.slice(name.lastIndexOf('.') + 1);
Every occurrence
Loop with the position argument.
let i = -1;
while ((i = s.indexOf(x, i + 1)) !== -1) hits.push(i);

Examples

1. First occurrence
'hello'.indexOf('l')
Returns
2
2. Last occurrence
'hello'.lastIndexOf('l')
Returns
3
3. Absent
'hello'.indexOf('z')
Returns
-1
4. Match at the start
'hello'.indexOf('h')
Returns
0
5. From a position
'hello'.indexOf('l', 3)
Returns
3
6. Empty needle
'abc'.indexOf('')
Returns
0

Pitfalls

1. A match at position 0 is falsy
The classic. `if (s.indexOf(x))` is false both when the text is absent (-1 is truthy, so it is actually TRUE then) and when it matches at the very beginning. The condition is wrong in both directions, and it looks perfectly reasonable.
Backwards
if ('hello'.indexOf('h')) { /* never runs */ }
0 is falsy
Be explicit
if ('hello'.indexOf('h') !== -1) { }
runs
2. > 0 misses the first position
A common attempt at a fix that introduces a subtler bug — it works for every match except one at index 0. Code reviewed this way passes tests that happen not to include a leading match.
Off by one case
'hello'.indexOf('h') > 0
false
Use includes
'hello'.includes('h')
true
3. lastIndexOf takes its position argument backwards
For indexOf, position is where to start looking forwards. For lastIndexOf it is the highest index at which a match may BEGIN, and the search runs backwards from there — so the same number means different things on the two methods.
Not "from index 3 on"
'hello'.lastIndexOf('l', 2)
2
Plain call
'hello'.lastIndexOf('l')
3
4. It does not reject a RegExp
Where includes throws, indexOf stringifies — so indexOf(/b/) searches for the literal four characters "/b/" and returns -1 on text that obviously contains a b. No error points at the mistake.
Searches "/b/"
'abc'.indexOf(/b/)
-1
Use search
'abc'.search(/b/)
1

When to use

Use it
  • You need the position, usually to slice around it
  • Finding the first or last delimiter in a path or key-value string
  • Walking every occurrence with the position argument
Reach for something else
  • You only want a yes/no answer → includes
  • Checking the beginning or end → startsWith, endsWith
  • You need a pattern → search, which returns an index too
  • Membership in a list → split then Array.includes

Notes

Complexity
O(n × m) worst case; engines use faster substring searches in practice
Return
A number; nothing is allocated or modified
CPython impl
V8: Builtins-string-indexof
Memory
No allocation
Thread-safe
Single-threaded; the string is only read

FAQ

It dates to C, where returning an out-of-band integer was the convention, and JavaScript inherited it via Java. It is the reason every membership test needs an explicit comparison — the failure value is truthy while a perfectly good result, 0, is falsy. includes exists precisely to avoid it.

s.indexOf(x) !== -1;   // correct
s.includes(x);         // clearer

History

ES1
indexOf and lastIndexOf present from the first version of the language.
ES2015
includes added, removing the need for the !== -1 comparison in the common case.