JavaScriptBitwise Operators

Bitwise Operators

Bitwise operators work on the binary representation of numbers. Most JavaScript you write will never touch them — string parsing, network requests and DOM manipulation rarely call for bit twiddling. They become genuinely useful in three places: bit flags, fast integer tricks, and code that interoperates with binary formats (TypedArrays, canvases, codecs). Worth knowing they exist, worth recognising them on sight.

The 32-bit truth

JavaScript numbers are 64-bit floats, but bitwise operators first convert their operands to a 32-bit signed integer, do the operation, then convert back to a regular number. That conversion is the source of a few surprises with very large numbers.

JS
(2 ** 31) | 0;     // -2147483648  (overflow into negative)
(2 ** 32) | 0;     // 0            (any bit above the 32nd is dropped)
3.9 | 0;           // 3            (handy way to truncate toward zero)
-3.9 | 0;          // -3
No bitwise for big numbers
If you need to do bitwise math on values beyond `±2^31`, use `BigInt` (which has its own bitwise operators) — the regular operators silently chop the top bits.
AND, OR, XOR, NOT

JS
0b1100 & 0b1010;   // 0b1000  = 8   (AND — bits set in BOTH)
0b1100 | 0b1010;   // 0b1110  = 14  (OR  — bits set in EITHER)
0b1100 ^ 0b1010;   // 0b0110  = 6   (XOR — bits set in exactly one)

~0;                // -1            (NOT — flip every bit)
~5;                // -6            (~n === -(n+1))
~~3.9;             // 3             (double NOT — another truncate trick)

The relationship ~n === -(n+1) falls out of two's-complement representation. ~~value is a popular shorthand for "convert to a 32-bit integer", though Math.trunc(value) reads more clearly.

Shift operators

JS
1 << 3;     // 8     (1 shifted left 3 places — same as 1 * 2^3)
16 >> 2;    // 4     (16 shifted right 2 — same as 16 / 4, integer)
-16 >> 2;   // -4    (signed: copies the sign bit)
-16 >>> 2;  // 1073741820  (unsigned: fills with zeros, treats as unsigned 32-bit)
  • << — left shift. Multiplies by 2^n (within 32 bits).

  • >> — sign-propagating right shift. Divides by 2^n, rounding toward negative infinity for negatives.

  • >>> — zero-fill right shift. Treats the value as an unsigned 32-bit int. Only operator that genuinely differs from regular arithmetic on negatives.

`>>> 0` is a common trick
`x >>> 0` converts `x` to a non-negative 32-bit integer. `Array` length internally uses this conversion. You'll see it in performance-sensitive code, but `Number(x)` and `Math.floor` are usually clearer.
Real uses #1: bit flags

When you have a small fixed set of yes/no options, packing them into a single integer saves space and makes "set / unset / test" operations one CPU instruction.

JS
const READ  = 1 << 0;   // 0b001 = 1
const WRITE = 1 << 1;   // 0b010 = 2
const EXEC  = 1 << 2;   // 0b100 = 4

// Combine flags
let permissions = READ | WRITE;       // 0b011 = 3

// Test a flag
const canWrite = (permissions & WRITE) !== 0;
console.log(canWrite);                // true

// Add a flag
permissions |= EXEC;                  // 0b111 = 7

// Remove a flag
permissions &= ~WRITE;                // 0b101 = 5

// Toggle a flag
permissions ^= READ;                  // 0b100 = 4

console.log(permissions);             // 4
true
4

Node's fs.constants, web event listener options, and many native APIs still expose flags this way. Recognising the pattern saves you a trip to the documentation.

Real uses #2: integer parsing and truncation

JS
// Truncate toward zero — no library call needed
3.9 | 0;    // 3
-3.9 | 0;   // -3
Math.trunc(3.9); // 3 — clearer, same result

// "Is x in range 0..N?" without two comparisons
// (x >>> 0) < N === (x >= 0 && x < N), as long as N <= 2^31
const inRange = (i >>> 0) < arr.length;
Don't optimise prematurely
Modern engines turn `Math.trunc` and friends into the same machine instruction as the bitwise tricks. Use bit twiddling only when it makes the *intent* clearer (flags) — not in pursuit of imaginary speed.
Real uses #3: low-level interop

When you parse a binary protocol, manipulate canvas pixels, or read from a Uint8Array, bitwise operators are unavoidable.

Pack RGBA into one integer

JS
function rgba(r, g, b, a) {
  return ((a & 0xff) << 24) |
         ((b & 0xff) << 16) |
         ((g & 0xff) << 8)  |
          (r & 0xff);
}

function unpack(p) {
  return {
    r: p         & 0xff,
    g: (p >> 8)  & 0xff,
    b: (p >> 16) & 0xff,
    a: (p >>> 24) & 0xff,   // unsigned shift to avoid sign issues
  };
}

const pixel = rgba(255, 128, 64, 255);
console.log(pixel.toString(16)); // "ff4080ff"
console.log(unpack(pixel));      // { r: 255, g: 128, b: 64, a: 255 }
BigInt has its own bitwise operators

For values outside the 32-bit range, the same operator symbols work on BigInt. They behave like infinitely-wide signed integers (no overflow).

JS
(1n << 40n);        // 1099511627776n
(1n << 40n) & 0xffn; // 0n
~0n;                 // -1n
When NOT to reach for bitwise
  • For booleans — use &&, ||, !. Bitwise & and | skip short-circuiting, which costs you the safety of guarded expressions.

  • For modular arithmetic on non-power-of-two moduli — % is what you want.

  • For division by arbitrary numbers — >> only works for powers of two.

  • For very large numbers — convert to BigInt first.

  • For readability — Math.trunc, Math.floor, and named flags read better than a wall of &, |, <<.

The bitwise litmus test
If you can explain the operation to a junior dev in one sentence (`"check the WRITE flag"`), use bitwise. If you'd need three sentences, prefer a clearer alternative — the engine isn't keeping score.