ESC

Type to search the knowledge base.

Bitwise Operators for Flags

Using & | ^ ~ << for permission flags and packed options — int32 traps, readability tradeoffs, and clearer alternatives.

advanced3 min read
  • javascript
  • bitwise
  • flags
  • permissions

Bitwise flags pack many booleans into one integer. You will meet them in file modes, canvas bitfields, and interview questions. You rarely need them for app feature toggles — a Set of strings is clearer — but you should read and write them without sweating.

Operators that matter

Op Meaning (per bit)
a | b set bits that are in either
a & b bits in both
a ^ b bits in one but not both
~a flip all bits (see int32 note)
a << n shift left (multiply by 2^n)
a >> n arithmetic shift right
a >>> n zero-fill shift right
const READ = 1 << 0;  // 0001
const WRITE = 1 << 1; // 0010
const EXEC = 1 << 2;  // 0100
const ALL = READ | WRITE | EXEC;

let perms = READ | EXEC; // 0101

// check
(perms & WRITE) !== 0; // false
(perms & READ) !== 0;  // true

// add
perms = perms | WRITE;

// remove
perms = perms & ~WRITE;

// toggle
perms = perms ^ EXEC;

Int32 coercion (the footgun)

All JS bitwise ops convert operands to signed 32-bit integers (except when you use BigInt). High bits vanish.

const big = 2 ** 40;
big | 0; // 0 — not what you wanted

// for wide flags, use BigInt
const FLAG = 1n << 40n;
const pack = FLAG | 1n;
(pack & FLAG) !== 0n; // true

~x is -(x + 1) for numbers in practice:

~0;  // -1
~1;  // -2
// masking after ~ often uses >>> 0 for unsigned 32-bit view
(~READ) >>> 0;

Compact option bags

const HAS_CHILDREN = 1 << 0;
const IS_EXPANDED = 1 << 1;
const IS_SELECTED = 1 << 2;

function encodeNode({ hasChildren, expanded, selected }) {
  let f = 0;
  if (hasChildren) f |= HAS_CHILDREN;
  if (expanded) f |= IS_EXPANDED;
  if (selected) f |= IS_SELECTED;
  return f;
}

function isSelected(flags) {
  return (flags & IS_SELECTED) !== 0;
}

Useful when serializing millions of tree nodes. Overkill for a React props object.

Enumerating set bits

function* setBits(n) {
  let i = 0;
  n >>>= 0; // force uint32
  while (n) {
    if (n & 1) yield i;
    n >>>= 1;
    i++;
  }
}

[...setBits(0b1011)]; // [0, 1, 3]

When not to use flags

// clearer for app code
const permissions = new Set(['read', 'write']);
permissions.has('write');
permissions.add('exec');
permissions.delete('write');

Flags win when: interop with native APIs, dense storage, or hot bit tests. They lose when: you need names in logs, partial updates from APIs, or more than 32 distinct bits without BigInt.

Interview answer

“I assign each flag a power of two, combine with |, test with &, clear with & ~flag, toggle with ^. JS bitwise ops use signed 32-bit ints, so I switch to BigInt for wider packs. For product code I often prefer a Set of permission strings unless density or interop requires bits.”

Further reading

Related guides