
When you use a bitwise operator in JavaScript the engine doesn’t do magic: it converts the Number to a 32‑bit integer, performs the operation on that 32‑bit integer, then converts the 32‑bit result back to a Number. The conversion rules are defined by ECMAScript’s ToInt32 and ToUint32. If you understand those two small algorithms you stop being surprised by negative results, zeros, and odd wraparound.
Here are the essentials, in plain programmer terms:
– ToUint32(x): take the mathematical value of x, if it’s NaN or ±Infinity treat it like 0, reduce it modulo 2^32 (so you keep the low 32 bits), return that unsigned integer in the range 0..2^32-1.
– ToInt32(x): do ToUint32(x), and if the result is ≥ 2^31 subtract 2^32 to get a signed 32‑bit value in the range -2^31..2^31-1.
Bitwise operators call these conversions implicitly. That means expressions like x | 0 are not “casting” in the C sense; they’re doing the ToInt32 dance, with truncation toward zero for fractional parts, NaN→0, ±Infinity→0, and modular wrapping for large integers.
console.log(1.9 | 0); // 1 (fractional part dropped) console.log(-1.9 | 0); // -1 (truncates toward zero) console.log(NaN | 0); // 0 console.log(Infinity | 0); // 0 console.log(-0 | 0); // 0 (-0 becomes +0)
The modulo/wrapping is where people frequently trip. 2^32 is the wrap point. If the unsigned 32‑bit value is bigger than 2^31-1 it becomes negative when interpreted as a signed 32‑bit integer.
console.log(4294967295 >>> 0); // 4294967295 (ToUint32 preserves it) console.log(4294967295 | 0); // -1 (ToInt32 interprets low 32 bits as signed) console.log(4294967296 | 0); // 0 (2^32 mod 2^32 == 0) console.log((2**31) | 0); // -2147483648 (2^31 is the most negative int32)
If you specifically want an unsigned 32‑bit value, use >> 0. That’s the canonical idiom:
let n = someNumber >>> 0; // converts to 0..4294967295
And if you want a quick signed int32:
let i = someNumber | 0; // converts to -2147483648..2147483647
Two small but critical details people forget:
1) Shift counts are masked. Only the lower 5 bits of the shift count are used (i.e. modulo 32). That means shifting by 32 is the same as shifting by 0, 33 same as 1, etc.
console.log(1 << 32); // 1 (32 → 0) console.log(1 << 33); // 2 (33 → 1)
2) All bitwise ops operate on signed 32‑bit integers internally (except that >> does a logical right shift, filling with zeros). So if you perform arithmetic that mixes bitwise results you may see negative numbers where you expected a big positive one.
console.log((1 << 31) | 0); // -2147483648 (highest bit set → negative) console.log((1 << 31) >>> 0); // 2147483648 (logical shift yields unsigned view)
Floating point precision matters before bitwise conversion. The Number type can’t precisely represent every integer above 2^53, so if you try to force insanely large integers into 32 bits, they may already have lost low bits.
let a = 2**53 + 1; console.log(a === 2**53); // true - the +1 vanished console.log((a | 0)); // whatever the truncated, already-imprecise value maps to
So the pipeline is: the Number is rounded/represented in double precision, then ToUint32/ToInt32 extracts the low 32 bits of that double‑precision value (after treating NaN/Infinity as 0), then the bitwise op runs on those bits, then the result is converted back to a Number. That conversion back is why you still get a Number and why weird things like negative zero vanish.
In practice: use |0 to coerce to a signed 32‑bit integer when you want fast truncation and don’t care about overflow; use >>0 when you need an unsigned 32‑bit result. But remember that shifts wrap counts modulo 32, and very large Numbers may have lost their low bits before the conversion. If you need true 64‑bit integer behavior, don’t rely on bitwise zero-cost tricks – use BigInt or an explicit library.
Small reference cheat sheet – copy these into your REPL when you’re uncertain:
console.log((123.9 | 0), (-123.9 | 0)); // trunc toward zero console.log((2**31) | 0); // -2147483648 console.log((2**31 - 1) | 0); // 2147483647 console.log((2**32 - 1) >>> 0); // 4294967295 console.log((-1) >>> 0); // 4294967295 console.log(1 << 31); // -2147483648 console.log(1 >>> 31); // 0 console.log(1 << 30); // 1073741824 console.log(1 << 31); // -2147483648 (signed) console.log(1 >>> 31); // 0 (logical)
When debugging, print both signed and unsigned interpretations – that often makes the intention clear. For instance, if you see a negative number coming out of some bitwise math, log both:
function debugBits(x) {
console.log('signed:', x | 0, 'unsigned:', x >>> 0, 'hex:', (x >>> 0).toString(16));
}
debugBits(4294967295);
debugBits(2**31);
And if bitwise logic still behaves strangely, check these in order: has the value lost low bits due to double-precision limits? did you accidentally shift by more than 31 (remember masking)? is NaN/Infinity creeping in? are you mixing typed arrays or WebAssembly results that are already 32‑bit? One last micro‑gotcha: using bitwise coercion as a micro-optimization (|0 instead of Math.trunc) is fine in hot inner loops, but modern engines are smart – readability often trumps a handful of cycles. If you’re optimizing, profile. Also remember that bitwise operators always return a Number, so if you want to treat it as an integer in arithmetic you still get ordinary floating point math afterward.
Example of a subtle bug: you assume masking with & keeps things positive, but because operands are converted to signed int32 before the op, the result might be negative. If you intended an unsigned mask, do (x & mask) >>> 0 so you get an unsigned result you can print or compare reliably.
let x = 0x80000000; // highest bit set console.log(x & 0xFFFFFFFF); // -2147483648 (signed treatment) console.log((x & 0xFFFFFFFF) >>> 0); // 2147483648 (unsigned view)
All this is why I recommend treating bitwise as “do this only when you mean 32‑bit integers”. If you find yourself sprinkling |0, >>0, and weird shifts to force types, write a tiny helper that documents the intent. That reduces the number of times future-you will stare at a negative number and wonder how a 2^32‑1 turned into -1. Example tiny helper:
console.log(4294967295 >>> 0); // 4294967295 (ToUint32 preserves it) console.log(4294967295 | 0); // -1 (ToInt32 interprets low 32 bits as signed) console.log(4294967296 | 0); // 0 (2^32 mod 2^32 == 0) console.log((2**31) | 0); // -2147483648 (2^31 is the most negative int32)
Those wrappers also make refactoring safer: replace micro-optimizations with clear names, and later when you need BigInt you only change the helper. Now, if you need to implement a tight loop that relies on 32‑bit overflow semantics, remember that the engine performs these conversions eagerly – so the loop will follow int32 wrapping rules and you can write code that intentionally uses that wrapping, for example implementing a small hash or a PRNG using multiply-and-add with |0. Example: a tiny xorshift-style update in 32 bits:
console.log(4294967295 >>> 0); // 4294967295 (ToUint32 preserves it) console.log(4294967295 | 0); // -1 (ToInt32 interprets low 32 bits as signed) console.log(4294967296 | 0); // 0 (2^32 mod 2^32 == 0) console.log((2**31) | 0); // -2147483648 (2^31 is the most negative int32)
That works because every intermediate is kept to the low 32 bits. But again: if you port to a language with true 32‑bit ints, the semantics are subtly different because JavaScript always converts back to Number after each expression, so use tests. And if you’re wondering about real CPU performance: sometimes the JIT will avoid repeated boxing/unboxing and keep values in registers, but you should
Oura Ring 5 Sizing Kit - Size Before You Buy Oura Ring 5 - Unique Sizing, Not Standard Ring Sizing - Receive Amazon Credit for Oura Ring 5 Purchase
$10.00 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Common bitwise operators explained with practical examples
With the conversion rules out of the way, let’s look at the operators themselves. They’re simpler than you think. You probably learned them in a computer science class and promptly forgot them, because who uses bitwise AND in a web app? Well, it turns out, you might, if you’re doing anything with graphics, file formats, or high-performance code.
The & (AND) operator is for checking bits. The rule is simple: a result bit is 1 only if the corresponding bits in both operands are 1. Its most common use is masking: using a “mask” value to check if certain flags are set in another value. Think of it as a filter.
// Let's define some flags. Using powers of two is the key.
const FLAG_READ = 1; // Binary ...0001
const FLAG_WRITE = 2; // Binary ...0010
const FLAG_EXEC = 4; // Binary ...0100
let userPermissions = 5; // This is FLAG_READ | FLAG_EXEC, or ...0101
// To check for a specific permission, we AND the values.
// If the result is not zero, the flag was set.
if ((userPermissions & FLAG_WRITE) !== 0) {
// This code will not run, because 5 & 2 is 0.
// 0101 (5)
// & 0010 (2)
// ------
// 0000 (0)
console.log("User has write permission.");
}
if ((userPermissions & FLAG_READ) !== 0) {
// This code WILL run, because 5 & 1 is 1.
// 0101 (5)
// & 0001 (1)
// ------
// 0001 (1)
console.log("User has read permission.");
}
The | (OR) operator is for setting bits. A result bit is 1 if the bit is 1 in either operand. This lets you combine flags without affecting the ones that are already set. It’s additive, in a sense.
let permissions = 0; // Start with no permissions. // Grant read permission. permissions = permissions | FLAG_READ; // 0 | 1 => 1 // Now grant execute permission. permissions |= FLAG_EXEC; // 1 | 4 => 5. The |= is just shorthand. console.log(permissions); // 5 // If we grant a permission that's already there, nothing changes. permissions |= FLAG_READ; // 5 | 1 => 5. It's idempotent.
Then there’s ^ (XOR), the “exclusive OR”. A result bit is 1 only if the bits in the operands are different. This operator is fantastic for toggling. If you XOR a value with a mask, any bit that is 1 in the mask gets flipped. Bits that are 0 in the mask are left alone. You don’t have to check the current state; you just flip it.
let options = 5; // ...0101 (options A and C are on) const MASK_C = 4; // ...0100 // Toggle option C. options = options ^ MASK_C; // 5 ^ 4 => 1 // 0101 (5) // ^ 0100 (4) // ------ // 0001 (1) -> Option C is now off. console.log(options); // 1 // Toggle it again. options ^= MASK_C; // 1 ^ 4 => 5 // 0001 (1) // ^ 0100 (4) // ------ // 0101 (5) -> Option C is back on.
The ~ (NOT) operator is the odd one out. It’s unary and it flips all 32 bits of its operand. This is where the signed 32-bit integer conversion becomes really visible. Flipping all bits of a positive number almost always results in a negative number, according to the rule ~x = -(x + 1). For example, ~5 is -6. Its most famous (or infamous) use was checking the return value of indexOf, which returns -1 on failure.
console.log(~5); // -6
console.log(~-1); // 0
let items = ['a', 'b', 'c'];
let position = items.indexOf('d'); // -1
if (~position) { // ~-1 is 0, which is falsy
console.log("Found it!"); // Does not run
} else {
console.log("Not found."); // Runs
}
This is a clever trick, but these days items.includes('d') is infinitely more readable, so you should probably use that instead. Save the bit-twiddling for when you’re actually twiddling bits, not just checking for -1.
Finally, we have the shift operators. They move bits left or right. << (Left Shift) moves bits to the left, filling in the right with zeros. This is a very fast equivalent of multiplying by powers of 2.
// 3 in binary is ...0011 console.log(3 << 1); // 6 (...0110). Same as 3 * 2. console.log(3 << 2); // 12 (...1100). Same as 3 * 4. // This is the canonical way to create flag values. const OPT_A = 1 << 0; // 1 const OPT_B = 1 << 1; // 2 const OPT_C = 1 << 2; // 4 const OPT_D = 1 << 3; // 8
The right shift is more complicated because there are two versions. >> is the “sign-propagating” or “arithmetic” shift. It moves bits to the right, but it fills the new bits on the left with a copy of the original sign bit. This has the effect of preserving the number’s sign, making it a fast division by powers of 2 for signed integers.
In contrast, >>> is the “zero-fill” or “logical” shift. It always fills the new bits on the left with zeros, regardless of the sign. This is the one you want when you are manipulating bit patterns like RGBA color values or data from a binary file, where the sign is meaningless and you just want to move bits around.
let positive = 20; // ...00010100 console.log(positive >> 2); // 5 (...00000101) console.log(positive >>> 2); // 5 (...00000101) // For positive numbers, they behave identically. let negative = -20; // ...11101100 (in 32-bit two's complement) // Sign-propagating shift fills with 1s console.log(negative >> 2); // -5 (...11111011) // Zero-fill shift fills with 0s console.log(negative >>> 2); // 1073741819 (...0011111011)
The huge positive number from >>> is why you have to be careful. If you’re working with data that just happens to have its high bit set, using >> will give you a negative number you probably weren’t expecting. When in doubt, if you’re thinking in terms of bits and not signed math, you probably want >>>.
Real-world uses for masks flags and micro-optimizations
The most common real-world use for bitwise operators is managing a set of boolean options, or “flags,” inside a single number. Instead of an object like { canRead: true, canWrite: false, canExecute: true }, you pack these states into the bits of an integer. This is ridiculously memory-efficient, but the real win is performance. Checking, setting, or clearing multiple flags can be done in a single, atomic CPU instruction, which is much faster than manipulating properties on a JavaScript object.
The pattern involves combining operators. You use | to set flags, and & with ~ to clear them. Checking for flags is also done with &.
const FLAG_A = 1 << 0; // 1
const FLAG_B = 1 << 1; // 2
const FLAG_C = 1 << 2; // 4
const ALL_FLAGS = FLAG_A | FLAG_B | FLAG_C;
let options = FLAG_A; // Start with A enabled
// Set FLAG_C
options |= FLAG_C; // options is now 5 (A and C)
// Clear FLAG_A
options &= ~FLAG_A; // options is now 4 (just C)
// Toggle FLAG_B
options ^= FLAG_B; // options is now 6 (C and B)
// Check if FLAG_C is set
if ((options & FLAG_C) !== 0) {
console.log("C is enabled.");
}
// Check if both B and C are set
const mask = FLAG_B | FLAG_C;
if ((options & mask) === mask) {
console.log("B and C are both enabled.");
}
This pattern is everywhere in low-level programming. A fantastic, practical example in the JavaScript world is color manipulation, common in graphics programming with Canvas or WebGL. A 32-bit number can perfectly represent an ARGB color value, with 8 bits for alpha (transparency), red, green, and blue. Bitwise operators are the tool for packing and unpacking these components.
// Let's pack an ARGB color into a single 32-bit integer.
let a = 255, r = 100, g = 150, b = 200;
// Shift each component into its 8-bit slot and OR them together.
let color = (a << 24) | (r << 16) | (g << 8) | b; // The result is a single number. Note it might be negative if 'a' > 127.
// Use >>> 0 to see the unsigned hex value.
console.log(`Packed color: ${color}, Hex: 0x${(color >>> 0).toString(16)}`);
// Packed color: -625516, Hex: 0xff6496c8
// Now, let's unpack it back into components.
let unpacked_a = (color >>> 24) & 0xFF;
let unpacked_r = (color >>> 16) & 0xFF;
let unpacked_g = (color >>> 8) & 0xFF;
let unpacked_b = color & 0xFF;
console.log(`A:${unpacked_a} R:${unpacked_r} G:${unpacked_g} B:${unpacked_b}`);
// A:255 R:100 G:150 B:200
Notice the use of >>>. We need a logical shift because we are treating the color as a bag of bits, not a signed number. If we used >>, the sign bit would propagate and corrupt the alpha component for any color with an alpha value over 127.
Beyond flags, bitwise operators are the foundation of many micro-optimizations. While modern JavaScript engines are incredibly smart and you should always profile before optimizing, some idioms are so common they are worth knowing. Using | 0 to truncate a float to an integer is often faster than Math.trunc() or Math.floor() in a hot loop, because it avoids a function call.
// Instead of this: let x = Math.floor(someValue / 10); // You might see this in performance-critical code: let x = (someValue / 10) | 0;
Another classic is checking for even or odd numbers. Modulo arithmetic (%) can be slower than a simple bitwise AND. Since the least significant bit is 1 for any odd number and 0 for any even number, you can check it directly.
function isEven(n) {
return (n & 1) === 0;
}
function isOdd(n) {
return (n & 1) === 1;
}
console.log(isEven(100)); // true
console.log(isOdd(101)); // true
And for a party trick that demonstrates the power of XOR, you can swap two integer variables without needing a temporary third variable. It relies on the properties that x ^ x = 0 and x ^ 0 = x.
let a = 5; // ...0101
let b = 12; // ...1100
a ^= b; // a becomes 5 ^ 12 = 9 (...1001)
b ^= a; // b becomes 12 ^ 9 = 5 (...0101)
a ^= b; // a becomes 9 ^ 5 = 12 (...1100)
console.log(a=${a}, b=${b}); // a=12, b=5
While this is clever, it’s less useful in a high-level language like JavaScript where readability is paramount and destructuring assignment ([a, b] = [b, a]) does the same job more clearly. But it’s a fundamental technique in systems programming and a good illustration of thinking in bits. These optimizations are not just academic. They are used in the real world in browser engines, game engines, WebAssembly modules, and Node.js core libraries where performance is not a feature but a requirement. When you are processing millions of operations per second, the difference between a function call and a single bitwise instruction adds up.
Pitfalls gotchas and how to debug bitwise logic
The single most common and infuriating bug you will encounter with bitwise operations has nothing to do with bits. It’s operator precedence. The bitwise operators &, ^, and | have a lower precedence than comparison operators like ==, !=, <, and >. This means an expression that looks perfectly correct is parsed in a way you did not intend.
const FLAG_ENABLED = 4; // ...0100
let options = 5; // ...0101
// You want to check if the flag is set.
// You write this, which looks right:
if (options & FLAG_ENABLED == FLAG_ENABLED) {
// This code will NEVER run.
console.log("Flag is enabled.");
}
Why doesn’t it work? Because of precedence, it’s evaluated as options & (FLAG_ENABLED == FLAG_ENABLED). That becomes 5 & (4 == 4), which is 5 & true. The & operator coerces true to the number 1, so you get 5 & 1, which is 1. The if statement then checks if (1), which is true, but that’s not what you were testing for! The correct code requires parentheses. Always. No exceptions. Burn this into your brain: always wrap bitwise expressions in parentheses when they are part of a larger expression.
// The correct way:
if ((options & FLAG_ENABLED) == FLAG_ENABLED) {
// This works as expected.
// (5 & 4) is 4. 4 == 4 is true.
console.log("Flag is enabled.");
}
// An even more robust check is to see if the result is non-zero.
if ((options & FLAG_ENABLED) !== 0) {
console.log("Flag is enabled (robust check).");
}
The second major gotcha is the signed-integer problem. All bitwise operators except >>> treat their operands as signed 32-bit integers. If an operation results in a value where the 32nd bit is 1, JavaScript will interpret that as a negative number. This can be baffling when you’re just trying to manipulate color channels or data flags.
// A 32-bit value representing a color, with the alpha channel at the top. // This color is fully opaque red: 0xFF0000FF let opaqueRed = 4278190335; // Let's try to extract the alpha channel (the top 8 bits) const ALPHA_MASK = 0xFF000000; let alpha = opaqueRed & ALPHA_MASK; console.log(alpha); // -16777216
You got a negative number because 0xFF000000 has its sign bit set. The result is correct in terms of its bits, but JavaScript’s default conversion back to a Number gives you the signed interpretation. When you debug, you need to see the underlying bit pattern. The easiest way is to do a zero-fill right shift by 0 (>>> 0), which forces an unsigned 32-bit interpretation, and then print it in hexadecimal.
function debugBits(n) {
const signed = n | 0;
const unsigned = n >>> 0;
const hex = '0x' + unsigned.toString(16).padStart(8, '0');
console.log({ signed, unsigned, hex });
}
let alpha = 4278190335 & 0xFF000000;
debugBits(alpha);
// { signed: -16777216, unsigned: 4278190080, hex: '0xff000000' }
This little helper function is your best friend. When a bitwise calculation gives you a weird number, throw it in debugBits. The hex string will almost always reveal the bug. You’ll see the bits you expected, and realize the problem is just the signed vs. unsigned interpretation.
Another pitfall is assuming that bitwise operations on numbers outside the 32-bit integer range will work on the full number. They won’t. JavaScript numbers are 64-bit floating-point values. When a bitwise operator is used, the number is first converted to a 32-bit integer. If your number is larger than what a 32-bit integer can hold, the upper bits are simply discarded. For truly large integer math, you must use the BigInt type, which has its own set of bitwise operators (&n, |n, etc.).
// A 64-bit integer value const bigNumber = 123456789012345n; // Note the 'n' for BigInt // A 64-bit mask const mask = 0xFFFFFFFFn; // Also a BigInt // Get the lower 32 bits const lower32 = bigNumber & mask; console.log(lower32.toString()); // 2722132713
Finally, a word on the infamous ~indexOf trick. For years, developers would check if an item was in an array like this: if (~arr.indexOf(item)). This works because indexOf returns -1 for “not found,” and ~-1 is 0, which is falsy. For any other index (0, 1, 2, …), the result of ~ is a non-zero number, which is truthy. While clever, this is terrible for readability. Today, you have Array.prototype.includes(). Use it. Your colleagues will thank you.
