Bitwise Operations in JavaScript

JavaScript secretly turns your numbers into 32-bit signed integers — here is exactly when and how it breaks, and what to do about it

JavaScript numbers are IEEE 754 64-bit floating-point. But every bitwise operator — &, |, ^, ~, <<, >>, >>> — first converts its operands to 32-bit signed integers via the ToInt32 abstract operation. This is an invisible step that silently truncates values, flips negative floats into large integers, and produces results that do not match your calculator. If you have been surprised by JavaScript bitwise output, you are not alone. Use the BitwiseCalc to cross-verify JavaScript results against the true mathematical bitwise value.

The 32-bit Conversion: What ToInt32 Actually Does

Before any bitwise operation, JavaScript runs this procedure on each operand:

// What JavaScript does internally (simplified):
// 1. Convert to integer (truncate fractional part)
// 2. Take modulo 2^32
// 3. Interpret as signed 32-bit (values >= 2^31 become negative)

// This is why:
console.log(0xFFFFFFFF & 0);     // -1, not 4294967295
console.log(3.9 & 0);            // 3 (truncated)
console.log(Math.pow(2, 33) | 0); // 0 (modulo 2^32 wraps to 0)

The operand 0xFFFFFFFF as a JS number is 4294967295. ToInt32 takes this modulo 232 (still 4294967295), then interprets the 32-bit pattern 0xFFFFFFFF as a signed integer, yielding -1. The AND with 0 is a no-op, so the result is -1. This is correct per the spec but surprises developers who expect unsigned results.

>> vs >>>: Arithmetic vs Logical Right Shift

JavaScript is unusual among high-level languages in having two right shift operators. >> is arithmetic (sign-extending): it copies the sign bit to preserve negative values. >>> is logical (zero-filling): it always fills with zeros from the left.

console.log(-8 >> 1);   // -4  (arithmetic: sign bit preserved)
console.log(-8 >>> 1);  // 2147483644 (logical: zeros fill from left)

The >>> operator is often used to force an unsigned 32-bit interpretation. (x >>> 0) is a common pattern to convert any number to an unsigned 32-bit integer — essentially the ToUint32 operation. This is useful when processing binary data from files or network protocols where you need unsigned values.

Practical JS Use Cases

Color Manipulation

CSS colors pack red, green, blue, and alpha into a single 32-bit integer. Bitwise operations extract and modify individual channels cleanly:

const color = 0xFF8844;  // orange-ish

const r = (color >> 16) & 0xFF;  // 0xFF = 255
const g = (color >> 8) & 0xFF;   // 0x88 = 136
const b = color & 0xFF;          // 0x44 = 68

// Reassemble: shift each channel and OR them together
const newColor = (r << 16) | (g << 8) | b;  // same as original

Fast Integer Truncation With |0

console.log(3.7 | 0);   // 3
console.log(-2.1 | 0);  // -2

x | 0 is a common JS idiom for fast float-to-int conversion. It is faster than Math.floor() for positive numbers but behaves differently with negatives: Math.floor(-2.1) gives -3, while -2.1 | 0 gives -2 (truncation toward zero, not toward negative infinity). Do not use it unless you specifically want truncation semantics.

Flag Enums for Feature Toggles

const FLAG_A = 1 << 0;  // 1
const FLAG_B = 1 << 1;  // 2
const FLAG_C = 1 << 2;  // 4

let features = FLAG_A | FLAG_C;  // 5 (A and C enabled)
features |= FLAG_B;              // 7 (add B)
features &= ~FLAG_A;             // 6 (remove A)

const hasA = (features & FLAG_A) !== 0;  // false
const hasB = (features & FLAG_B) !== 0;  // true

This is the same pattern used by React's effect flags and Node.js file system constants. Use the 32-bit programming calculator to prototype flag combinations before coding them.

BigInt Bitwise: No 32-bit Limit

ES2020 introduced BigInt, which supports bitwise operators without the 32-bit conversion:

const big = 0xFFFFFFFFFFFFFFFFn;  // BigInt literal (note the 'n')
console.log(big & 0n);  // 0xFFFFFFFFFFFFFFFFn — all bits preserved
console.log(big >> 32n); // 0xFFFFFFFFn — correct 64-bit shift

BigInt bitwise operators work at arbitrary precision, just like Python. However, you cannot mix BigInt and Number in the same operation — 5n & 3 throws a TypeError. And >>> is not supported for BigInt because logical right shift is meaningless without a fixed bit width. For 64-bit operations, use BigInt; for legacy Number-based code, use the 64-bit programmer calculator to verify your masks and shifts.

The ~indexOf() Gotcha

This is the most famous JavaScript bitwise footgun:

const idx = "hello".indexOf("x");  // -1 (not found)

if (~idx) {  // ~(-1) = 0, which is falsy
    console.log("found");
} else {
    console.log("not found");  // this branch runs — correct
}

~(-1) equals 0, which is falsy. For any other index ~x is non-zero (truthy). This used to be a common pattern before .includes() existed. Today, just write str.includes(substring) — it is clearer and avoids the ToInt32 footgun entirely.

JavaScript bitwise operations work, but you need to know the 32-bit signed conversion is always happening under the hood. For anything beyond hobby projects, use BigInt or verify your Number-based results against the BitwiseCalc to catch silent truncation bugs before they reach production.