Bitwise operations and two's complement for programmers
Most application code never touches individual bits, until it does: a permissions field stored as a bit mask, a binary protocol, a hash function, a color value, a network mask, or a bug where a large number suddenly turns negative. This guide explains the bitwise operators, shows the patterns developers actually use them for, and explains two's complement, the representation of negative numbers that causes most of the confusion. You can try every example in the Programmer Calculator.
Bits, bytes and bases
Integers are stored as fixed-width sequences of bits: 8 bits in a byte, and typically 32 or 64 bits in an int or long. Each bit is a power of two, so the 8-bit value 00101101 is 32 + 8 + 4 + 1 = 45.
Binary is long to write, so developers use hexadecimal: each hex digit represents exactly four bits, and a byte is exactly two hex digits. 0x2D is 0010 1101, which is 45. That clean mapping is why memory addresses, colors (#FF8800), hashes and byte dumps are written in hex. Octal, where each digit is three bits, survives mainly in Unix file permissions like 0755.
The bitwise operators
Bitwise operators work on each bit position independently:
a = 1100
b = 1010
a & b = 1000 AND: 1 where both are 1
a | b = 1110 OR: 1 where either is 1
a ^ b = 0110 XOR: 1 where they differ
~a = 0011 NOT: every bit flipped (shown in 4 bits)
Shifts move all bits left or right:
x << nshifts left by n positions, filling with zeros. Each shift multiplies by 2, as long as no bits fall off the top.x >> nshifts right. For signed types this is usually an arithmetic shift, which copies the sign bit so negative numbers stay negative; each shift divides by 2, rounding toward negative infinity.x >>> n, in Java and JavaScript, is a logical shift that always fills with zeros.
Practical patterns
Flags in a single integer
Assign each option one bit, then combine them with OR:
const READ = 1 << 0; // 0001
const WRITE = 1 << 1; // 0010
const DELETE = 1 << 2; // 0100
const ADMIN = 1 << 3; // 1000
let perms = READ | WRITE; // 0011
const canWrite = (perms & WRITE) !== 0; // test a flag
perms |= DELETE; // set a flag
perms &= ~WRITE; // clear a flag
perms ^= ADMIN; // toggle a flag
This stores many booleans in one column or field and makes checks fast. Unix file permissions, feature flags, event masks and many C APIs work this way.
Extracting fields
Shift and mask to read part of a value. To get the red component of a 24-bit color 0xFF8800: (color >> 16) & 0xFF gives 255; green is (color >> 8) & 0xFF, 136. The same technique parses packed binary protocols and IP addresses.
Quick checks
(x & 1) === 0tests whether x is even.x & (x - 1)clears the lowest set bit; it is zero exactly when x is a power of two (for x > 0).x & (n - 1)equalsx % nwhen n is a power of two, which hash tables use to pick buckets.
Two's complement: how negative numbers are stored
A fixed number of bits can represent a fixed number of values. With 8 bits, there are 256 patterns. Unsigned interpretation uses them for 0 to 255. Signed interpretation, in almost every modern CPU, uses two's complement for -128 to 127.
The rule: to negate a number, invert all its bits and add one.
5 = 0000 0101
~5 = 1111 1010
-5 = 1111 1011 (inverted, plus one)
The highest bit acts as the sign: 0 for non-negative, 1 for negative. Two's complement is used because ordinary binary addition works without special cases: adding 0000 0101 (5) and 1111 1011 (-5) gives 1 0000 0000, and the ninth bit falls off, leaving 0.
The same bits mean different numbers depending on the interpretation. 1111 1111 is 255 unsigned and -1 signed. 0x80000000 is 2,147,483,648 as an unsigned 32-bit value and -2,147,483,648 as a signed one. This is why a value read from a binary file or database can appear negative: the bits are right, the interpretation is not.
Overflow
When a result does not fit in the available bits, the extra bits are discarded and the value wraps around. In a signed 32-bit integer, 2,147,483,647 + 1 becomes -2,147,483,648. Languages handle this differently:
- Java and C# wrap silently for
intandlong; useMath.addExactorcheckedblocks to throw instead. - In C and C++, signed overflow is undefined behaviour, which compilers may exploit in surprising ways. Unsigned overflow wraps.
- Rust panics on overflow in debug builds and wraps in release builds unless you use explicit methods like
checked_addorwrapping_add. - Python integers have arbitrary precision and never overflow.
JavaScript's special case
JavaScript numbers are 64-bit floating point, but bitwise operators first convert their operands to 32-bit signed integers. So 2 ** 31 | 0 is -2147483648, and 0xFFFFFFFF | 0 is -1. Use >>> 0 to reinterpret a result as unsigned. For 64-bit values, such as IDs or hashes, use BigInt: (1n << 63n) - 1n works exactly, and BigInt.asIntN and BigInt.asUintN wrap to a chosen width.
Habits that avoid bit bugs
- Always state the width and signedness when sharing a binary or hex value.
- Use named constants for masks and shifts instead of magic numbers.
- Parenthesise bitwise expressions; in C, Java and JavaScript,
==binds tighter than&, sox & MASK == 0does not do what it looks like. - Use unsigned types or logical shifts when working with raw bytes.
- Check intermediate values in a calculator that wraps like the real type, rather than a decimal desk calculator that never overflows.