includes()

ES6+

Determines whether one string can be found inside another, returning true or false. The search is case-sensitive, compares UTF-16 code units, and can optionally begin at a given position. Introduced in ES2015, it expresses a yes-or-no containment question directly, where older code had to compare indexOf() against -1.

Syntax

string.includes(searchString, position)

Parameters

searchString string

The string to search for

position number optional

Position to start searching from

Return Value

boolean

true if the string is found, otherwise false

Examples

JavaScript
const str = 'Hello World';
console.log(str.includes('World'));
console.log(str.includes('world'));
Output:
// true false

📌 When to Use

Use includes() whenever the question is simply 'does this string contain that one?': search-as-you-type filters, keyword and banned-word checks, feature detection in user-agent or header strings, and quick guards like checking that an email field contains an @ before deeper validation. It returns a boolean, so it slots naturally into if conditions, filter() and some() callbacks, and logical expressions without the !== -1 ceremony that indexOf() requires, and without the falsy-zero trap that made indexOf() conditions subtly dangerous. Reach for indexOf() instead only when you need the position of the match, for example to slice around it or to highlight it; reach for startsWith() or endsWith() when the location is constrained to an edge, because those methods state the constraint and reject matches elsewhere; and reach for a regular expression with test() when the 'substring' is really a pattern with alternation or classes. The search is strictly case-sensitive and literal, so case-insensitive matching means lowercasing both sides first, and includes() will happily match inside words: a banned-word filter looking for 'ass' flags 'classic', the classic Scunthorpe problem, so word-boundary-sensitive matching needs a regex. The optional position argument starts the search partway through, useful when scanning past a known prefix.

⚠️ Common Mistakes

Forgetting that the search is case-sensitive: 'Hello World'.includes('world') is false. Any user-facing search or filter almost always wants both sides normalized with toLowerCase() first; forgetting one side is the most frequent cause of 'search is broken' reports.

Passing a regular expression: includes() throws a TypeError when given a regex, it does not silently convert it. Pattern matching belongs to regex test() or match(); includes() is strictly for literal substrings.

Substring matching where whole-word matching was intended: includes('cat') is true for 'concatenate'. Content filters and keyword detectors built on bare includes() generate false positives inside longer words; word boundaries require a regex with the boundary assertion or explicit tokenization.

Using it for security decisions: url.includes('trusted.com') is true for 'evil-trusted.com.attacker.io'. Substring presence is not origin validation; parse the URL and compare the hostname exactly, or at minimum use startsWith()/endsWith() on the parsed host.

Not knowing that every string includes the empty string: str.includes('') is always true, so an empty search box matches everything. Decide explicitly whether empty input should mean 'match all' or 'match none' and handle it before calling includes().

Coercion surprises with non-string arguments: the argument is converted to a string, so '12345'.includes(23) works but 'null check'.includes(null) is true because it searches for the text 'null'. Pass strings deliberately rather than relying on coercion.

✅ Best Practices

Prefer includes() over indexOf() !== -1 for pure containment checks; it reads as the question you are asking and eliminates the class of bugs where someone writes if (str.indexOf(x)) and position 0 evaluates falsy.

Normalize case once, outside the loop: lowercase the query a single time, then compare against lowercased candidates inside filter callbacks, rather than lowercasing the query per item.

Use the most specific method available: startsWith() and endsWith() for anchored checks, includes() only when the match may appear anywhere, so the code documents where the text is expected.

Combine includes() with some() or every() to test a string against a list of terms, as in terms.some(t => text.includes(t)), which keeps multi-keyword logic declarative.

For repeated searching of large text bodies on every keystroke, debounce the input and consider pre-normalizing the searched fields once, since the per-call lowercasing dominates the cost.

Do not build allow/deny security logic on substring presence; parse the value (URL, path, MIME type) into components and compare those exactly.

⚡ Performance Notes

includes() performs a straightforward substring search, linear in the haystack length in practice, and engines implement it with efficient memory-comparison primitives, so it is fast: scanning even hundreds-of-kilobytes strings takes microseconds. It compiles no pattern, allocates nothing, and is therefore cheaper than an equivalent regex test() and identical in cost to indexOf(), which shares the same search algorithm underneath. In realistic search-filter code the expense is rarely the containment check itself but the normalization around it: calling toLowerCase() on every field of every item per keystroke allocates far more than includes() ever will, which is why pre-lowercasing data or hoisting query normalization matters more than the choice of search method. The optional position argument can skip a known prefix and marginally shrink the scan, but its real value is correctness, not speed.

🌍 Real World Example

Content Filtering and Search Features

Three containment checks at different trust levels. searchProducts() implements the everyday shop filter: lowercase the query once, then keep products whose name or description includes it, matching how users expect search boxes to behave. isAllowedUrl() and containsBannedWords() both scan a list with some() and includes(), a compact idiom for 'does any term appear'. They also illustrate the method's limits worth teaching: substring presence in a URL is a weak security signal compared with parsing the hostname, and bare substring matching flags banned words inside innocent longer words, so production moderation adds word boundaries or tokenization on top of this skeleton.

// Simple search filter for products
function searchProducts(products, query) {
  const lowerQuery = query.toLowerCase();
  return products.filter(product =>
    product.name.toLowerCase().includes(lowerQuery) ||
    product.description.toLowerCase().includes(lowerQuery)
  );
}

const products = [
  { name: 'iPhone 15', description: 'Apple smartphone' },
  { name: 'Galaxy S24', description: 'Samsung smartphone' },
  { name: 'MacBook Pro', description: 'Apple laptop' }
];

console.log(searchProducts(products, 'apple'));
// Returns iPhone 15 and MacBook Pro

// Check if URL is from allowed domains
function isAllowedUrl(url, allowedDomains) {
  return allowedDomains.some(domain =>
    url.toLowerCase().includes(domain.toLowerCase())
  );
}

const allowed = ['example.com', 'trusted.org'];
console.log(isAllowedUrl('https://api.example.com/data', allowed)); // true
console.log(isAllowedUrl('https://malicious.com/data', allowed)); // false

// Content moderation - check for banned words
function containsBannedWords(text, bannedWords) {
  const lowerText = text.toLowerCase();
  return bannedWords.some(word =>
    lowerText.includes(word.toLowerCase())
  );
}

const banned = ['spam', 'scam', 'fake'];
console.log(containsBannedWords('This is a scam!', banned)); // true

Related Methods