JavaScript's bitwise operators work on 32-bit signed integers behind the scenes. This guide covers all six operators, the quirks of the 32-bit conversion, when to use BigInt, real patterns for flags and color parsing, and when bits beat Math.
JavaScript gives you the same six operators as C or Java: & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), >> (signed right shift), and >>> (unsigned right shift). The last one is JavaScript-specific — most C-family languages do not have it as a distinct operator.
const a = 0b1100; // 12 const b = 0b1010; // 10 console.log(a & b); // 8 (0b1000) console.log(a | b); // 14 (0b1110) console.log(a ^ b); // 6 (0b0110) console.log(~a); // -13 (flips all 32 bits) console.log(a << 1); // 24 console.log(a >> 1); // 6 console.log(a >>> 1); // 6 — same for positive numbers
Each operator converts its operands to 32-bit signed integers, does the operation, and converts back to a JavaScript Number (64-bit float). This is the key thing to understand before using them in production code.
JavaScript numbers are 64-bit IEEE 754 floats under the hood. But bitwise operators do something surprising: they first call ToInt32() on each operand, converting it to a 32-bit signed integer. This means any bits beyond position 31 are discarded, and fractional parts are chopped off.
// Surprising truncation const big = 0xFFFFFFFF + 1; // 4294967296 — fits in a JS Number console.log(big | 0); // 0 — because only the low 32 bits survive // Fractional chop console.log(3.7 | 0); // 3 — used as a fast floor() by some devs console.log(-3.7 | 0); // -3 // Values outside 32-bit signed range wrap around console.log(2147483648 | 0); // -2147483648 (wraps to INT32_MIN)
I ran into this when building a hex color parser. A 24-bit color value is safe, but anything using bits beyond position 31 silently breaks. The fix is BigInt, which I cover next.
JavaScript has two right shift operators. >> copies the sign bit into the leftmost positions, preserving the sign. >>> always fills with zeros. With negative numbers, these give wildly different results:
const neg = -8; console.log(neg >> 2); // -2 — sign preserved console.log(neg >>> 2); // 1073741822 — treated as unsigned 32-bit // Why? -8 as 32-bit unsigned is 0xFFFFFFF8 // Shifting right by 2 with zero-fill gives 0x3FFFFFFE = 1073741822
ES2020 introduced BigInt, which supports arbitrary-precision integers and bitwise operations on them. The syntax is nearly identical — just append n to your literals. BigInt bitwise ops do not truncate to 32 bits.
// BigInt bitwise operations — no 32-bit limit const bigA = 0xABCD1234ABCD1234n; const bigB = 0xFFFF0000FFFF0000n; console.log((bigA & bigB).toString(16)); // "abcd0000abcd0000" console.log((bigA | bigB).toString(16)); // "ffff1234ffff1234" console.log((bigA << 32n).toString(16)); // shifts work at full precision // But: BigInt and Number do not mix // const x = 5 & 3n; // TypeError!
Note that BigInt does not support >>>. Signed right shift is the only option, and since BigInt has no fixed width, there is no concept of an unsigned shift anyway. When you need the unsigned behavior, mask after shifting: (n >> k) & mask.
Instead of passing multiple booleans to a function, pack them into one integer. Each bit is a flag. This is how the DOM's Node.compareDocumentPosition() and many Web APIs work.
const FLAG_READ = 1 << 0; // 1 const FLAG_WRITE = 1 << 1; // 2 const FLAG_ADMIN = 1 << 2; // 4 let permissions = 0; permissions |= FLAG_READ | FLAG_WRITE; // 3 // Check a flag const canWrite = (permissions & FLAG_WRITE) !== 0; // true // Toggle admin permissions ^= FLAG_ADMIN; // adds admin (3 | 4 = 7) permissions ^= FLAG_ADMIN; // removes admin (7 ^ 4 = 3)
Converting a hex color string to RGB channels is a two-step process: parse the hex, then extract each channel with shifts and AND masks.
function hexToRgb(hex) {
const n = parseInt(hex.replace("#", ""), 16);
return {
r: (n >> 16) & 0xFF,
g: (n >> 8) & 0xFF,
b: n & 0xFF
};
}
const { r, g, b } = hexToRgb("#4A90D9");
console.log(`R:${r} G:${g} B:${b}`); // R:74 G:144 B:217
n & 1 is the fastest way to check if a number is odd. It is a single CPU instruction versus n % 2 which does division. For hot loops processing thousands of values, this adds up.
const values = [3, 7, 12, 9, 4, 8, 15];
// Separate evens and odds in one pass
const evens = [];
const odds = [];
for (const v of values) {
(v & 1 ? odds : evens).push(v);
}
console.log(evens); // [12, 4, 8]
console.log(odds); // [3, 7, 9, 15]
A non-zero number is a power of two exactly when it has a single bit set. The test (n & (n - 1)) === 0 checks this in O(1). This pops up all the time — canvas sizing, buffer allocations, bitmask validation.
function isPowerOfTwo(n) {
return n > 0 && (n & (n - 1)) === 0;
}
console.log(isPowerOfTwo(64)); // true
console.log(isPowerOfTwo(100)); // false
console.log(isPowerOfTwo(0)); // false — edge case handled
JavaScript's bitwise operators convert operands to 32-bit signed integers before operating. This comes from the ECMAScript specification, which chose 32-bit for consistency with common CPU register sizes. Numbers larger than 2^31-1 or with fractional parts get truncated. For 64-bit operations, use BigInt (e.g., 0xABCD1234n & 0xFFFF0000n).
The >> operator (signed right shift) propagates the sign bit, preserving the sign of negative numbers. The >>> operator (unsigned right shift) fills from the left with zeros regardless of the sign. For example, -8 >> 2 = -2, but -8 >>> 2 = 1073741822 (because -8 becomes a large unsigned 32-bit number).
Use bitwise operators when working with flags, binary protocols, color manipulation, or performance-critical integer math. For example, x | 0 is a fast floor for positive 32-bit numbers, and n & 1 checks parity faster than n % 2. However, they sacrifice readability — use them when the performance gain justifies it.
Test JavaScript bitwise patterns directly in the browser with our interactive bitwise calculator. See binary, hex, and decimal results update in real time.