Lodash _.includes() Method: A Practical, Edge-Case-Driven Guide

I still see bugs rooted in one tiny question: “Is this value inside my collection?” It sounds trivial, but edge cases pile up fast—NaN comparisons, negative offsets, array-like data, and strings that quietly behave differently from arrays. When that question lives inside validation rules, feature flags, or access checks, a single false negative can cause a cascade of issues. That’s why I keep Lodash’s _.includes() in my toolkit, even in 2026 with modern JavaScript features.

You’re about to get a practical mental model of how _.includes() behaves with arrays, objects, and strings; when the index parameter changes everything; and how SameValueZero equality avoids the NaN pitfall that still trips people up. I’ll also show how I use it in real workflows, where I prefer native methods, and where I do not. You’ll walk away with patterns you can trust, plus a checklist for avoiding the most common mistakes.

What _.includes() Really Checks

When I reach for _.includes(), I’m choosing predictable membership checks across different collection types. The method accepts arrays, array-like values, objects, and strings. If the collection is a string, it looks for a substring. Otherwise it compares values using SameValueZero, which is the same equality semantics used by Array.prototype.includes().

Why that matters: SameValueZero treats NaN as equal to NaN, and it treats -0 and 0 as the same value. That behavior is often what you want for membership checks. In contrast, indexOf() fails to find NaN, which can silently break validation if you’re filtering user input or data from a sensor feed.

I think of _.includes() like a universal “contains” operator with consistent rules. It’s not magic, but it’s uniform: arrays and objects are checked with SameValueZero; strings are searched by substring; and you can start from a given index, even from the end with a negative offset.

Syntax and Parameters Without the Hand-Waving

Here’s the precise call signature and what each parameter does in practice.

_.includes(collection, value, index)
  • collection: The data structure you inspect. This can be an array, object, or string. If it’s an object, Lodash checks the object’s values, not its keys.
  • value: The value you’re looking for.
  • index: The start position. Defaults to 0. If negative, it becomes an offset from the end of the collection.

It returns a boolean: true if the value is found, otherwise false.

Here is a fully runnable example with a realistic set of values:

const _ = require("lodash");

const categories = ["backend", "frontend", "infra", "security", "data"];

console.log(_.includes(categories, "security"));

console.log(_.includes(categories, "mobile"));

console.log(_.includes(categories, "backend", 2));

You should see:

true

false

false

That last line matters. Starting from index 2 skips the earlier items, so it does not see "backend" at index 0.

Strings Aren’t Arrays, and That’s the Point

When the collection is a string, _.includes() behaves like a substring search. That’s different from array membership, but it’s consistent with how developers usually think about strings.

I use this for quick checks like input sanitation or feature flag tags embedded in a string.

const _ = require("lodash");

const banner = "release-2026.01-hotfix";

console.log(_.includes(banner, "hotfix"));

console.log(_.includes(banner, "release-2025"));

console.log(_.includes(banner, "2026", 10));

The third call starts from index 10. That means it won’t match "2026" at the front if the offset skips past it. This behavior is easy to miss if you treat the index parameter as optional and then later pass a negative or positive value without thinking about string offsets.

If you want case-insensitive checks on strings, I recommend normalizing first:

const _ = require("lodash");

const role = "Admin";

// Normalize to lowercase before checking

console.log(_.includes(role.toLowerCase(), "admin"));

That’s not Lodash-specific, but it avoids a lot of production issues.

The Negative Index Trick I Use All the Time

Negative index values are easy to ignore, but they’re powerful. A negative index tells Lodash to start searching from the end of the collection. If you pass -1, it starts at the last element. If you pass -3, it starts three from the end.

I use this for “recent items” checks, like validating the last few events in a log or recent user actions.

const _ = require("lodash");

const recentEvents = ["login", "view", "export", "logout", "login"];

// Search only the last 2 events

console.log(_.includes(recentEvents, "export", -2));

// Search the last 4 events

console.log(_.includes(recentEvents, "export", -4));

If you run that, the first line prints false and the second prints true. That’s because -2 starts two from the end and skips the earlier “export.”

This offset-from-end rule matches how I reason about “recent history.” It’s a very small feature that often replaces a slice-plus-contains pattern in my code.

SameValueZero and Why It Saves You From NaN Bugs

A surprising number of production bugs come from NaN comparisons. If you’re comparing floats or reading data from APIs that might produce NaN, indexOf() fails quietly:

const values = [1, 2, NaN, 4];

console.log(values.indexOf(NaN)); // -1

_.includes() handles NaN properly because it uses SameValueZero.

const _ = require("lodash");

const values = [1, 2, NaN, 4];

console.log(_.includes(values, NaN)); // true

That single difference can save you from a subtle security bug or from data validation that silently passes when it should fail. In my experience, NaN often shows up in analytics pipelines, sensor data, and user-generated spreadsheets. _.includes() is a safe guardrail here.

Objects: Values, Not Keys

When the collection is an object, Lodash checks the object’s values. That’s a detail many developers forget. I make it explicit in my own code with a short comment to avoid confusion in reviews.

const _ = require("lodash");

const featureFlags = {

allowBeta: true,

showNewUI: false,

enableAudit: true,

};

// Object check is against values, not keys

console.log(_.includes(featureFlags, true));

console.log(_.includes(featureFlags, "showNewUI"));

The first line is true because there are true values. The second line is false because Lodash does not inspect keys. If you need to check keys, I use Object.prototype.hasOwnProperty or .has() rather than forcing .includes() to do something it does not do.

Real-World Patterns I Use in 2026

Even with modern native features, I still use _.includes() in a few recurring patterns.

1) Input validation with mixed collection types

When I’m writing a validation helper and the input might be an array or a string, I let Lodash handle the differences.

const _ = require("lodash");

function isAllowed(label, allowed) {

// allowed can be an array or a comma-separated string

if (typeof allowed === "string") {

return _.includes(allowed, label);

}

return _.includes(allowed, label);

}

console.log(isAllowed("pro", ["free", "pro", "team"]));

console.log(isAllowed("team", "free,pro,team"));

This is simple, but it keeps API surfaces flexible. I use it in internal tooling where config can be a string or an array, depending on how it was loaded.

2) Event gating in async workflows

When I build job processors or message queues, I check event names against an allowed list. I like _.includes() here because I sometimes inspect object values, not just arrays.

const _ = require("lodash");

const allowedEvents = ["create", "update", "delete", "archive"];

function shouldProcess(eventName) {

return _.includes(allowedEvents, eventName);

}

console.log(shouldProcess("archive"));

console.log(shouldProcess("restore"));

3) TypeScript-friendly wrappers

I often wrap Lodash calls to keep typings clean and make intent obvious.

const _ = require("lodash");

// Small wrapper for clarity in code reviews

function containsValue(collection, value, index = 0) {

return _.includes(collection, value, index);

}

console.log(containsValue(["east", "west"], "east"));

That wrapper can evolve later into a more constrained helper if you decide to accept only arrays or only strings.

Traditional vs Modern Approaches: What I Recommend

I still prefer native methods for simple array checks in modern JavaScript, especially when you’re already in a tight loop. But _.includes() is a better fit when collection types are not uniform or you want a single behavior across arrays, objects, and strings.

Use Case

Traditional Approach

Modern Approach I Prefer —

— Array membership

arr.indexOf(value) !== -1

arr.includes(value) or _.includes(arr, value) when types vary NaN detection

Manual Number.isNaN checks

_.includes(arr, NaN) for arrays Object value search

Object.values(obj).includes(value)

_.includes(obj, value) for clarity Substring search

str.indexOf(substr) !== -1

_.includes(str, substr) or str.includes(substr)

My recommendation: use native includes when you control the data type, and Lodash _.includes() when you want one call that handles arrays, objects, and strings in a single path. That reduces branching and makes code more readable in shared libraries.

Common Mistakes I See (and How to Avoid Them)

1) Assuming object keys are checked

I’ve already mentioned it, but it’s worth repeating: .includes() checks object values, not keys. If you need keys, use Object.keys() or .has().

2) Forgetting the negative index behavior

A negative index is not “search everywhere,” it’s “start from the end.” If you pass -1, you’re only checking the last element. I add a comment in code when I use a negative index to avoid confusion.

3) Comparing objects by structure

_.includes() uses SameValueZero, which still compares objects by reference. Two object literals that look the same are not equal unless they are the same reference.

const _ = require("lodash");

const a = { id: 1 };

const b = { id: 1 };

console.log(_.includes([a], b)); // false

console.log(_.includes([a], a)); // true

If you need structural checks, use _.some() with a predicate or a deep comparison helper.

4) Blindly trusting string searches

_.includes("admin", "ad") returns true. That’s not always what you want for permission checks. If you need exact token matches, split the string first or use a stricter parse.

Performance Notes Without the Myths

_.includes() is linear for arrays and strings, so it walks until it finds the match or reaches the end. For most real-world usage, that’s fast enough. On larger arrays (tens of thousands of items), it is still typically quick, often within a few milliseconds. When I need repeated membership checks against large lists, I use a Set instead.

A simple rule I follow:

  • One or two checks: _.includes() or native includes.
  • Many checks against the same data: build a Set and use has().

Here’s how I wrap that idea in code for readability:

function createMembershipChecker(values) {

const set = new Set(values);

return (value) => set.has(value);

}

const hasPermission = createMembershipChecker(["read", "write", "admin"]);

console.log(hasPermission("admin"));

This doesn’t replace _.includes(); it complements it. I use Lodash when inputs are mixed, and I reach for Set when I do repeated checks in performance-sensitive loops.

When I Use It and When I Don’t

I use _.includes() when:

  • I’m working with a collection that might be an array, object, or string.
  • I want NaN-safe checks without extra logic.
  • I’m building shared utilities where consistent semantics matter more than micro-performance.

I avoid it when:

  • I need exact token matching inside strings.
  • I’m doing heavy repeated checks against a large static list (I use Set).
  • I already have strict typing and known collection types (native methods are fine).

This is the same rule set I share with teams: consistency in utilities; specificity in performance-critical areas.

Practical Edge Cases You Should Test

If you’re using _.includes() in a shared library or in a critical data path, I suggest testing these cases explicitly:

const _ = require("lodash");

console.log(_.includes([0, -0], 0));

console.log(_.includes([NaN], NaN));

console.log(_.includes("release-2026.01", "2026", -6));

console.log(_.includes({ a: 1, b: 2 }, 2));

console.log(_.includes([{ id: 1 }], { id: 1 }));

The last example is the object reference pitfall. In my code reviews, I flag it immediately, because it usually signals a deeper misunderstanding about object identity.

AI-Assisted Workflows in 2026

I rely on AI tools daily, but I still verify behavior like this by running small local scripts. If you use an AI assistant to generate code, ask it to include a membership test suite that covers NaN, negative indexes, and object values. That small harness catches most semantic mistakes before they hit a feature branch.

In team settings, I also recommend a shared snippet collection with these tests. It’s a light way to prevent regressions when developers swap between native includes and Lodash _.includes() without realizing the behavioral difference.

Key Takeaways and Next Steps

If your code has even a small chance of receiving mixed collection types, I recommend standardizing on _.includes() for membership checks. It gives you a consistent rule set, it handles NaN correctly, and it keeps your code readable. The behavior around strings, object values, and negative indexes is the part that deserves your attention—those are the spots where bugs hide.

For immediate next steps, I’d suggest running a quick audit in your own codebase:

  • Identify any indexOf checks that might be hiding NaN issues.
  • Replace cross-type membership logic with a single _.includes() call.
  • Add tests that cover negative indexes and object reference equality.

If you do that, you’ll remove a class of subtle bugs and make the intent of your checks obvious to anyone who reads your code. That’s the kind of quiet improvement that pays off every time you onboard a new teammate or revisit a module six months later.

Deeper Mental Model: How Lodash Normalizes Collections

When I’m explaining _.includes() to teammates, I frame it as a two-step process: normalization, then comparison. Lodash takes your input and decides which “collection rules” to apply. If it’s a string, it treats it as a sequence of characters and looks for a substring. If it’s not a string, Lodash treats it like a list of values. That’s the key: an object’s values become the list, not its keys.

This approach is why I call .includes() a universal “contains.” It isn’t uniform in how it parses each type, but it is uniform in how it answers the question: “Do any values match?” That is the same mental model I use when reading code. If I see .includes() on a plain object, I immediately know it’s about values. If I see it on a string, I know it’s a substring check. I don’t have to inspect the implementation every time.

That said, I do not expect it to behave like a regular expression or a structured parser. It’s a simple member check, not a semantic search. When I need a more precise match—like a token match in a space-separated string—I split the string or use a dedicated parser first.

Array-Like Collections and Why They Matter

Lodash supports array-like values, which means structures with a length property and indexed elements. This is one of the big reasons I still reach for _.includes() in utility libraries, because real-world code often deals with array-like data in unexpected places.

Examples I see all the time:

  • arguments in older code
  • DOM NodeList values
  • Typed arrays such as Uint8Array
  • Custom objects that look like arrays because of how data is marshaled

Here’s a pragmatic example with a NodeList pattern (in a browser environment):

const _ = require("lodash");

const nodeList = document.querySelectorAll(".tag");

// NodeList is array-like, not a real array

console.log(_.includes(nodeList, nodeList[0]));

I don’t always need Lodash for this, but I like that _.includes() handles array-like values without extra conversion. If I used native includes, I’d need to convert with Array.from() or spread syntax first. That might be fine for one-off usage, but I avoid it in shared utilities where I want a single, consistent interface.

Unicode, Graphemes, and String Surprises

Strings can be trickier than they look. Most string membership checks work on code units, not user-perceived characters (graphemes). _.includes() follows normal JavaScript string behavior: it does a straightforward substring search by code unit.

That means emoji or complex characters may not behave the way you expect if you’re counting “characters” by eye. For example, a single emoji might be two code units, and some composed characters might be a sequence. This doesn’t mean .includes() is wrong; it means string membership checks are not semantic. If you need grapheme-aware matching, I normalize and split with a grapheme-aware library, then use .includes() on the resulting array.

Here’s a small example that illustrates the idea without pulling in heavy tooling:

const _ = require("lodash");

const text = "I love 🍕";

// Simple substring check works for code units

console.log(_.includes(text, "🍕"));

If you work with multilingual content or emojis in user input, keep this limitation in mind. It’s not a Lodash problem; it’s a property of JavaScript strings.

How I Think About Index Behavior in Practice

Developers often treat index as optional, and then later add it to optimize or modify behavior. That’s where subtle bugs creep in. I use a three-part rule of thumb:

1) index is an inclusive starting position, not a count.

2) Negative index is relative to the end, not a reverse search.

3) The search still goes forward, even when starting near the end.

I’ll underline the third point with an example. Developers sometimes assume a negative index searches backward. It doesn’t. It just shifts the starting point and then searches forward. That matters for strings, where searching forward from near the end might skip what you actually wanted to check.

const _ = require("lodash");

const version = "v2.7.0";

// This checks only the last character or two

console.log(_.includes(version, "v2", -1)); // false

When I use negative indexes, I usually write a quick comment in code to make the intent explicit: “check last N events only” or “limit search to recent items.” That small habit prevents confusion in future reviews.

Substrings vs Tokens: Avoiding Auth and Routing Bugs

One of the riskiest mistakes I see is substring-based permission checks. Consider a naive approach like this:

const _ = require("lodash");

const role = "superadmin";

// This returns true because "admin" is a substring

console.log(_.includes(role, "admin"));

That might be okay for a UI badge display, but it’s not safe for authorization. I’ve seen real-world bugs where “guestadmin” was mistakenly treated as “admin.” If I want token accuracy, I parse the string or use a proper role structure.

Here’s a safer pattern I prefer:

const _ = require("lodash");

const roles = "reader,editor,admin";

const roleList = roles.split(",").map((r) => r.trim());

console.log(_.includes(roleList, "admin"));

This makes the intended semantics explicit: you’re checking membership in a list of tokens, not a substring.

Structural Equality: When to Switch to Predicates

Because .includes() compares by reference for objects, it’s not suitable for deep equality. I consider that a feature, not a bug—it keeps .includes() fast and predictable. But when I need to match structures, I use _.some() with a predicate, or I compare specific fields.

For example, to check if any object in a list has id === 42, I do this:

const _ = require("lodash");

const users = [{ id: 1 }, { id: 42 }, { id: 99 }];

const hasUser = _.some(users, (u) => u.id === 42);

console.log(hasUser); // true

This is more explicit and avoids false expectations about deep equality. I use _.includes() only when I truly want identity equality or primitive comparisons.

Comparing _.includes() With Native Methods

I often get asked: “Why not just use native includes everywhere?” The honest answer is that I do, when I can guarantee the input type. But in shared utilities, you can’t always guarantee that. _.includes() protects you from that variability.

Here’s how I choose:

  • Known array: arr.includes(value)
  • Known string: str.includes(value)
  • Known object values: Object.values(obj).includes(value)
  • Unknown or mixed: _.includes(collection, value)

This isn’t about performance dogma; it’s about reducing conditional branching and simplifying readability. I would rather call one method than add multiple branches that re-implement Lodash’s logic.

Designing APIs That Accept Flexible Collections

A lot of my API helpers accept flexible inputs because configuration can come from environment variables, JSON, or UI-based toggles. For example, a whitelist might arrive as a CSV string in production and as an array in tests. Instead of over-normalizing, I let _.includes() do the heavy lifting.

Here’s a more realistic version of the earlier validation helper with normalization and safety:

const _ = require("lodash");

function normalizeAllowed(allowed) {

if (Array.isArray(allowed)) return allowed;

if (typeof allowed === "string") {

return allowed.split(",").map((s) => s.trim()).filter(Boolean);

}

if (allowed && typeof allowed === "object") {

// Use values for objects

return Object.values(allowed);

}

return [];

}

function isAllowed(label, allowed) {

const list = normalizeAllowed(allowed);

return _.includes(list, label);

}

console.log(isAllowed("pro", ["free", "pro", "team"]));

console.log(isAllowed("team", "free,pro,team"));

console.log(isAllowed("beta", { a: "beta", b: "alpha" }));

I prefer to normalize in a helper because it makes the membership checks more explicit. But even without normalization, _.includes() gives me a stable baseline.

Guarding Feature Flags and Config

Feature flags are a great example of where _.includes() shines. I often see flags stored as arrays in one environment and as strings in another (especially when they come from CLI args). I like to normalize, but I also like to keep checks simple.

const _ = require("lodash");

const flags = process.env.FEATURE_FLAGS || "new-ui,fast-path";

const enabledFlags = flags.split(",").map((f) => f.trim());

function isEnabled(flag) {

return _.includes(enabledFlags, flag);

}

console.log(isEnabled("new-ui"));

This might look too simple, but it’s exactly the kind of code that breaks when you rely on indexOf and forget about NaN or edge-case type coercion. Lodash makes that mistake much less likely.

Differences That Matter in Tests

When I write tests around membership checks, I deliberately include “weird” values. My goal is to catch what I call “silent breaks.” Those are the ones that only show up with unusual input: NaN, empty strings, negative indexes, and object references.

Here’s a test-oriented snippet I use to validate behavior before writing final code:

const _ = require("lodash");

const cases = [

() => _.includes([1, 2, 3], 2) === true,

() => _.includes([1, 2, 3], 4) === false,

() => _.includes([NaN], NaN) === true,

() => _.includes([0, -0], -0) === true,

() => _.includes("abc", "b") === true,

() => _.includes("abc", "d") === false,

() => _.includes(["a", "b"], "a", 1) === false,

() => _.includes(["a", "b"], "b", -1) === true,

];

console.log(cases.map((fn) => fn()));

I’m not saying you need to test every single edge case in every project. But if you maintain shared utilities, these sanity checks prevent regressions.

Performance Considerations With Real-World Scale

I avoid exact numbers because performance is highly variable by environment, but I use a consistent rule of thumb:

  • Under a few thousand items: use _.includes() or native includes without worrying.
  • Tens of thousands, repeated checks: build a Set once.
  • Hundreds of thousands: restructure or index the data.

This is a practical, production-friendly guideline. I’ve seen teams spend weeks micro-optimizing membership checks when a simple Set would have solved the problem. And I’ve seen the opposite: overusing Set for tiny arrays, creating more code complexity than performance benefit.

Here’s how I wrap a Set for repeated checks in a clean way:

function createSetChecker(values) {

const set = new Set(values);

return {

has: (value) => set.has(value),

size: () => set.size,

};

}

const checker = createSetChecker(["read", "write", "admin"]);

console.log(checker.has("admin"));

I prefer returning a tiny object instead of a bare function when I want a reusable utility. It makes it easier to extend later without breaking existing usage.

Migrating From indexOf() to _.includes()

If you’re maintaining older code, you’ll see a lot of indexOf(...) !== -1. The migration to _.includes() is usually straightforward, but I do it carefully to avoid subtle changes.

Here’s a checklist I use:

1) Replace indexOf with _.includes for arrays.

2) If the data can contain NaN, write a test for it.

3) For strings, ensure you still want substring behavior.

4) For objects, double-check you’re not accidentally switching to value search.

Example migration:

// Old

if (items.indexOf(value) !== -1) {

// ...

}

// New

if (_.includes(items, value)) {

// ...

}

I treat this as a behavior-preserving change in most cases, but I still check for NaN and object references to be safe.

Practical Scenarios You’ll Encounter

Here are some scenarios where _.includes() tends to be the best fit, along with the reasoning I use.

Scenario 1: Mixed configuration sources

If your config values can come from a JSON file, environment variables, or a UI, you can end up with arrays, strings, or objects. _.includes() keeps your check logic stable while you normalize or evolve the config format.

Scenario 2: Log analysis and auditing

I often check log event types or tags. With negative indexes, I can limit searches to recent entries without slicing. That means less memory churn and cleaner code.

Scenario 3: Feature flag rollouts

Flags are frequently stored in a string for easy deployment. _.includes() plus a quick split gives me reliable checks with minimal boilerplate.

Scenario 4: Data cleanup in pipelines

When filtering noisy data, I often check for sentinel values (like null, undefined, or NaN). _.includes() is reliable for the NaN case, which is otherwise easy to miss.

When I Avoid It, Even If It “Works”

There are times when _.includes() is the wrong tool, even if it technically works. The main reason is semantics. If the membership logic is core to security or financial decisions, I want the code to be as explicit as possible, even if that means more lines.

Examples:

  • Permission checks: I prefer explicit role parsing and exact match.
  • Payments or billing status: I prefer enums or a Set with known values.
  • Data schemas: I prefer validation libraries that enforce structure.

I’m not saying _.includes() is unsafe, but I avoid ambiguous logic in high-stakes areas. The more critical the decision, the more explicit I want the code to be.

Debugging Tips: What I Do When It Returns Unexpected Results

When _.includes() surprises me, it’s almost always one of these issues:

1) Wrong collection type: The data is an object, but I thought it was an array.

2) Implicit string search: I passed a string and expected token matching.

3) Negative index: The starting offset skipped the value.

4) Reference mismatch: I compared objects by structure instead of by reference.

My debugging approach is to log the type, confirm the index, and inspect the actual values being compared. A quick console.log(typeof collection, Array.isArray(collection)) usually clears it up.

Practical Checklist for Safe Usage

I keep this checklist for myself, and it’s saved me from more regressions than I can count:

  • Is the collection a string? If yes, do I want substring behavior?
  • Is the collection an object? If yes, do I want values or keys?
  • Could the data include NaN? If yes, _.includes() is safer than indexOf.
  • Am I using a negative index? If yes, add a comment for clarity.
  • Am I comparing objects? If yes, I’m comparing references, not structure.
  • Is this performance critical with repeated checks? If yes, use a Set.

If I can answer those questions confidently, I ship it.

Expanding the Practical Example: A Small Utility Module

I like to show a small module that demonstrates how I use _.includes() in real utility code. This module handles mixed configuration inputs and keeps membership checks clear.

const _ = require("lodash");

function normalizeList(input) {

if (Array.isArray(input)) return input;

if (typeof input === "string") return input.split(",").map((s) => s.trim()).filter(Boolean);

if (input && typeof input === "object") return Object.values(input);

return [];

}

function createWhitelistChecker(input) {

const list = normalizeList(input);

return {

allows(value) {

return _.includes(list, value);

},

size() {

return list.length;

},

};

}

const checker = createWhitelistChecker("alpha,beta,gamma");

console.log(checker.allows("beta"));

console.log(checker.size());

This sort of wrapper makes it obvious that _.includes() is part of a larger normalization story. I like that, because it prevents future contributors from mixing membership logic with parsing logic.

Common Pitfalls in Code Reviews (My Red Flags)

When I review code that uses _.includes(), I look for a few patterns that usually indicate a bug:

  • _.includes(obj, key): This implies the developer intended to check keys. I immediately ask for clarification.
  • _.includes(str, "admin") for roles: This is a substring check; it can be dangerous.
  • _.includes(list, { id: 1 }): This almost always should be a predicate check, not reference equality.
  • Negative index without a comment: I ask for a short clarifying comment.
  • _.includes() inside a hot loop: I ask whether a Set would be more efficient.

These are easy to spot and easy to fix, which is why I prioritize them.

A Simple Analogy I Use When Teaching It

When explaining to newer developers, I use a simple analogy: _.includes() is like checking whether a box contains a specific item. If the box is a list, you check each item. If the box is a dictionary, you only check its values. If the box is a sentence, you look for a phrase.

That analogy is not perfect, but it gets the key idea across: the behavior depends on the container type. It’s simple and memorable, and it helps them avoid the most common mistakes.

Final Recommendations I Stand By

Here’s my distilled view after years of using it in real systems:

  • Use _.includes() when you want a consistent membership check across arrays, objects, and strings.
  • Use native includes when you control the type and want slightly less overhead.
  • Use a Set for repeated checks against large lists.
  • Never use _.includes() for object structural equality; use a predicate instead.
  • Always test NaN and negative index behavior if the data can include them.

If you follow those rules, you’ll avoid most of the subtle bugs that come from membership checks. That’s the reason I keep Lodash’s _.includes() in my toolbox: it’s not just convenience, it’s correctness in the messy places where data types and edge cases collide.

Key Takeaways and Next Steps (Expanded)

I’ll close with a tighter, expanded set of actions you can take today:

1) Search your codebase for indexOf(...) !== -1 and flag the ones that might interact with NaN or mixed collection types.

2) Replace those with _.includes() or native includes based on type certainty.

3) Add a small, reusable test harness covering NaN, negative indexes, objects, and strings.

4) Document a team guideline: “Use _.includes() for mixed types; native includes for strict types.”

5) Review any string-based permission checks for substring risks.

Do that, and you’ll noticeably reduce “it worked yesterday” bugs. The improvement is subtle but real—exactly the kind of improvement that makes your codebase calmer and more predictable over time.

Scroll to Top