All articles
Tutorials

Bitwise Operators for Boolean Flags in JavaScript

Learn how bitwise AND, OR, XOR, and NOT pack multiple boolean flags into a single integer, why the check is 1.5x faster than array lookup, and where the pattern appears in production systems.

Bitwise Operators for Boolean Flags in JavaScript cover
10 min read

TL;DR

  • A bitmask packs multiple yes/no flags into a single integer. Each flag occupies one bit position; the number 5 (binary 0101) means positions 0 and 2 are on, positions 1 and 3 are off.
  • Four operators cover all flag manipulation: OR sets flags, AND checks them, AND NOT clears them, XOR toggles them. Each operation is a single CPU instruction.
  • A bitwise AND check is 1.5x faster than .includes() on an array and uses 46% less memory than storing the same flags as boolean object properties.
  • The pattern is not new. Unix chmod 755, TCP header flags, and game engine entity state all use it. For the full permission system implementation, see Encoding RBAC as a JWT Integer Without Redis.

Most developers have seen code like user.permissions & CAN_DELETE and moved on without understanding what the & does or why it is written that way. The expression is not clever syntax. It is a lookup that runs in a single CPU instruction because the entire permission set lives inside one number.

This article explains how binary numbers encode multiple states, how the four bitwise operators manipulate them, and where this pattern shows up in production so you can read and write it with confidence.

How binary numbers work

Humans count with ten digits (0 through 9). When a position runs out of digits, it carries over to the next column:

8, 9, 10, 11 ... 98, 99, 100

Computers use two digits: 0 and 1, because transistors have two states: off and on. They carry over at 2 instead of 10:

0, 1, 10, 11, 100, 101, 110, 111, 1000 ...

This is base 2, or binary. The positional rule is the same as decimal: each column is worth the base raised to that position's power. In decimal, columns are worth 1, 10, 100. In binary, columns are worth 1, 2, 4, 8:

position:  3   2   1   0
value:     8   4   2   1

The number 5 in binary is 0101. Position 2 is on (worth 4), position 0 is on (worth 1). The others are off:

position:  3   2   1   0
bit:       0   1   0   1
value:     0 + 4 + 0 + 1 = 5

Using bits to represent feature flags

Once you see that each bit position is an independent on/off slot, it becomes possible to use a single integer as a set of named boolean states. Assign a meaning to each position:

position:  3          2              1            0
meaning:   beta       notifications  analytics    dark mode
bit:       0          1              0            1

A user with dark mode and notifications enabled is stored as 0101, which equals 5. All four settings live in one integer.

Bitmask visualization: the number 5 (binary 0101) decoded into four independent flag positions: Dark Mode ON, Analytics OFF, Notifications ON, Beta Access OFF

This is a bitmask: a single integer used to encode multiple boolean states, one per bit position.

Packing multiple settings into one integer

The conventional approach stores each flag as a named property:

const userSettings = {
  darkMode: true,
  analytics: false,
  notifications: true,
  betaAccess: false,
};

This is readable, but each object carries the property name strings, the values, and object overhead. Across millions of records, that adds up. Storing 100,000 of these objects uses 12.05 MB versus 6.47 MB for the equivalent bitmask integers.

The bitmask approach assigns each flag a power of two using the left-shift operator, which is shorthand for "1 shifted left by n positions." The pattern makes each constant's bit position explicit and prevents the typos that come from writing the powers of two by hand:

lib/flags.ts
const FLAGS = {
  DARK_MODE:     1 << 0,  // 0001 = 1
  ANALYTICS:     1 << 1,  // 0010 = 2
  NOTIFICATIONS: 1 << 2,  // 0100 = 4
  BETA_ACCESS:   1 << 3,  // 1000 = 8
} as const;

A user with dark mode and notifications enabled combines the two flags with OR:

const userSettings = FLAGS.DARK_MODE | FLAGS.NOTIFICATIONS;
// 0001 | 0100 = 0101 = 5

One number. Four settings. The object with its property name strings no longer exists.

Bitmask packing: a single number (5, binary 0101) fans out to four named flag states

The four operations

Every flag manipulation maps to one of four bitwise operators. Each is a single CPU instruction.

  1. Setting flags with OR

OR turns bits on. If either operand has a 1 in a position, the result has a 1 there. Use it to enable one or more flags at once:

let settings = 0;                             // 0000 — nothing set
settings = FLAGS.DARK_MODE | FLAGS.ANALYTICS; // 0001 | 0010 = 0011 = 3
OR operation: DARK_MODE (0001) combined with NOTIFICATIONS (0100) via the | operator produces 0101 (5)
  1. Checking flags with AND

AND tests whether a specific bit is on. The result is non-zero only if both operands share a 1 in the same position. Non-zero means the flag is set; zero means it is not:

const settings = 5; // 0101
 
if (settings & FLAGS.DARK_MODE) {
  // 0101 & 0001 = 0001 — non-zero, dark mode is on
}
 
if (settings & FLAGS.ANALYTICS) {
  // 0101 & 0010 = 0000 — zero, analytics is off
}
AND operation: checking Analytics (0010) against settings (0101) gives zero (off); checking Dark Mode (0001) gives non-zero (on)
  1. Clearing flags with AND NOT

To turn a specific bit off without touching any other, first use ~ to flip all bits in the flag, then AND the result against your value. The flipped mask is all 1s except the one position you want to clear:

let settings = 5; // 0101: dark mode on, notifications on
 
settings = settings & ~FLAGS.DARK_MODE;
// ~FLAGS.DARK_MODE = ~0001 = 1110
// 0101 & 1110 = 0100 = 4: dark mode off, notifications still on
AND NOT operation: NOT flips DARK_MODE from 0001 to 1110; AND with settings (0101) clears that position, leaving 0100
  1. Toggling flags with XOR

XOR flips a bit regardless of its current state: 1 becomes 0, 0 becomes 1. It is the natural operator for toggle switches:

let settings = 5; // 0101: analytics off
 
settings ^= FLAGS.ANALYTICS; // 0101 ^ 0010 = 0111 = 7: analytics now on
settings ^= FLAGS.ANALYTICS; // 0111 ^ 0010 = 0101 = 5: analytics off again

Why this matters at scale

The performance advantage comes from how the check executes. A bitwise AND is a single instruction that the CPU evaluates in one cycle. There are no function calls, no heap allocation, no string comparisons. Checking settings & FLAGS.DARK_MODE against checking settings.includes("dark_mode") on an array of string names:

ApproachOps/secondvs baseline
Array .includes()2,093,325baseline
Bitmask & check3,012,3251.5x faster

Memory tells a similar story. Across 100,000 user objects with four flags each:

ApproachTotalPer object
Multiple boolean properties12.05 MB126 bytes
String array of flag names11.58 MB121 bytes
Single bitmask integer6.47 MB68 bytes

At 10 million users, the bitmask approach saves roughly 558 MB compared to boolean properties. A single integer assignment is also atomic at the hardware level; updating three separate boolean properties is not, so a read between writes can observe a partially updated state.

Where this pattern already exists

Once you know the pattern, you see it everywhere systems were built for performance.

Unix file permissions encode read, write, and execute for owner, group, and others as nine bits spread across three groups. Running chmod 755 sets owner to 7 (binary 111: all three on), group to 5 (binary 101: read and execute on, write off), and others to 5. Nine independent permissions, stored and checked as a single integer.

TCP packet headers carry a flags byte that encodes SYN, ACK, FIN, RST, PSH, and URG. When a server receives a packet, it checks flags & SYN to detect a connection request. One byte, six independent states, billions of times per second.

Game engines track entity state (visible, collidable, active, paused, destroyed) as an integer per entity. The game loop checks flags for thousands of objects per frame. Allocating a boolean object for each would slow the loop down noticeably.

For the production implementation of this pattern in an auth permission system, see Encoding RBAC as a JWT Integer Without Redis.

When this breaks down

32-flag ceiling with bitwise operators. JavaScript's |, &, ~, and ^ coerce operands to signed 32-bit integers, giving you 31 usable positions before the sign bit. You can extend to 53 by using regular arithmetic instead of bitwise operators, or go beyond 53 with BigInt:

lib/flags.ts
const FLAGS = {
  FLAG_1:  1n << 0n,
  FLAG_33: 1n << 32n,
  FLAG_65: 1n << 64n,
} as const;
 
const hasFlag = (value: bigint, flag: bigint): boolean => (value & flag) !== 0n;

Raw values in logs. console.log(settings) prints 5, not ["DARK_MODE", "NOTIFICATIONS"]. A small helper that maps set bits back to names resolves this during development. Keep it out of the hot path; it is for debugging only:

lib/flags.ts
export function debugFlags(value: number): string[] {
  return Object.entries(FLAGS)
    .filter(([, flag]) => (value & (flag as number)) !== 0)
    .map(([name]) => name);
}
 
// debugFlags(5) => ["DARK_MODE", "NOTIFICATIONS"]

No dynamic flags. Bit positions must be assigned at compile time. If your application lets users define their own flag types at runtime, you need a different approach: a junction table or a serialized set.

Bit position stability. Once a permission occupies a position, that position is taken permanently. Reusing it for a different flag corrupts any stored integers and any tokens that carry the old value. Treat the flag schema like a database migration: only ever add positions, never reassign them.

This pattern works best when the flag set is fixed at deploy time, checks run in a hot path, and the number of flags stays under the integer ceiling. For small, infrequently-checked flag sets, the readability of named properties may outweigh the performance difference.

Key takeaways

A bitmask stores multiple boolean flags in one integer: OR sets bits, AND checks them, AND NOT clears them, and XOR toggles them, each in a single CPU instruction with no heap allocation. At scale, a bitmask check runs roughly 1.5x faster than an array include and uses half the memory of individual boolean properties across millions of objects. JavaScript's bitwise operators coerce to signed 32-bit integers, giving you 31 usable positions before the sign bit; switch to BigInt arithmetic when your flag set exceeds that limit. Bit positions are permanent once assigned: treat the flag schema like a database migration and only ever add new positions, never reassign existing ones.

Share𝕏

Writer

  • Wale Bashir

    Technical content writer and full-stack engineer with experience across Web3, AI, and backend systems.

Need help with your technical content?

We help B2B SaaS teams turn complex products into clear documentation and content that developers actually use.

Book a call
Bitwise Operators for Boolean Flags in JavaScript | Reclear