Searching Strings
Searching inside strings is one of the most common tasks in everyday JavaScript: validating user input, filtering lists, parsing URLs, or highlighting matches. The language gives you a small but expressive toolkit, from simple boolean checks like includes to position-aware lookups like indexOf and full pattern matching with regular expressions. Knowing which method to reach for keeps your code readable and avoids reinventing logic that the standard library already handles. This page walks through each option and when to use it.
Checking for presence with includes
String.prototype.includes answers a simple yes/no question: does this substring appear anywhere in the string? It returns a boolean, which makes it perfect for if conditions. It is case-sensitive and was introduced in ES2015.
const headline = "The quick brown fox";
console.log(headline.includes("quick")); // true
console.log(headline.includes("Quick")); // false (case-sensitive)
console.log(headline.includes("fox", 10)); // true (start searching at index 10)
Output:
true
false
true
The optional second argument is the index to start searching from. To make the check case-insensitive, normalize both sides first with toLowerCase().
Tip: Prefer
includesoverindexOf(x) !== -1when you only care about presence. It reads as plain English and removes the easy-to-forget-1comparison.
Finding positions with indexOf and lastIndexOf
When you need the location of a match (not just whether it exists), use indexOf. It returns the zero-based index of the first occurrence, or -1 if the substring is not found. lastIndexOf does the same but searches from the end, returning the last occurrence.
const path = "/users/profile/avatar.png";
console.log(path.indexOf("/")); // 0 (first slash)
console.log(path.lastIndexOf("/")); // 14 (last slash)
console.log(path.indexOf("xyz")); // -1 (not found)
// Extract the file name after the last slash
const fileName = path.slice(path.lastIndexOf("/") + 1);
console.log(fileName); // "avatar.png"
Output:
0
14
-1
avatar.png
Both methods accept an optional starting position. Because they return -1 for “not found”, always compare explicitly rather than relying on truthiness — index 0 is a valid match but is falsy.
Matching the start and end
To test whether a string begins or ends with a given substring, use startsWith and endsWith. They are clearer and faster to read than slicing or regex for these common checks.
const url = "https://devcraftly.com/docs";
console.log(url.startsWith("https://")); // true
console.log(url.endsWith(".com")); // false
console.log(url.endsWith("/docs")); // true
// endsWith can pretend the string is shorter
console.log(url.endsWith(".com", 22)); // true (treats length as 22)
Output:
true
false
true
true
startsWith accepts an optional position to begin from, while endsWith accepts an optional “end position” that treats the string as if it were that length — handy for checking a prefix of a longer string.
Method comparison
| Method | Returns | Case-sensitive | Use when |
|---|---|---|---|
includes | boolean | yes | You only need presence |
indexOf | number / -1 | yes | You need the first position |
lastIndexOf | number / -1 | yes | You need the last position |
startsWith | boolean | yes | Checking a prefix |
endsWith | boolean | yes | Checking a suffix |
search | number / -1 | regex-driven | Position of a pattern match |
match | array / null | regex-driven | Extracting matched text or groups |
Pattern matching with search and match
When a fixed substring is not enough — say you want “any digit” or “a word at a boundary” — reach for regular expressions. search is the regex-aware cousin of indexOf: it returns the index of the first match or -1.
const log = "Error 404: page not found";
console.log(log.search(/\d+/)); // 6 (index of "404")
console.log(log.search(/warning/i)); // -1 (no match)
Output:
6
-1
match goes further and returns the matched text. Without the global flag it returns an array containing the full match plus any capture groups; with the /g flag it returns every match as a flat array (or null if nothing matches).
const text = "Contact us at [email protected] or [email protected]";
const emailPattern = /[\w.]+@[\w.]+/g;
const emails = text.match(emailPattern);
console.log(emails);
console.log(emails?.length ?? 0);
Output:
[ '[email protected]', '[email protected]' ]
2
Warning:
matchreturnsnull(not an empty array) when there are no matches. Guard with optional chaining (result?.length) or the nullish coalescing operator before iterating, or you will hit aTypeError.
For richer extraction — capture groups with index information — matchAll returns an iterator of all matches, which you can spread into an array. Regex details are covered on their own page; this is just the tease.
Best practices
- Use
includes,startsWith, andendsWithfor plain substring checks — they are self-documenting and avoid-1comparisons. - Reserve
indexOf/lastIndexOffor when you actually need the numeric position, and always compare against-1explicitly. - Normalize case with
toLowerCase()on both operands when you need case-insensitive matching. - Reach for
search/matchonly when a fixed substring cannot express the pattern; a literal string check is faster and clearer. - Always guard
matchresults againstnullbefore accessing.lengthor iterating. - Prefer
matchAllover a globalmatchwhen you need capture groups or match indices.