Bitwise Operations and Flags

Table of Contents

  1. Why Work With Individual Bits?
  2. AND, OR, XOR, and NOT
  3. Shifts
  4. Bitmasks: Packing Many Yes/No Values Into One Number
  5. Unix Permissions and chmod 755
  6. Feature Flags
  7. Handy Bit Tricks
  8. The 32-Bit Caveat in JavaScript
  9. Recipe: bits.js
  10. Try It Yourself

1. Why Work With Individual Bits?

In the last chapter you saw that a number is a row of bits. Sometimes it is useful to treat that row as many separate switches instead of one quantity.

Imagine a user in an app can have these permissions: read, write, delete, share, export, admin. You could add six true/false columns to a database. Or you could use one number, where each bit is one permission. That is smaller, easy to copy, and fast to check. File systems, network protocols, graphics libraries, and game engines all do this.

To do it you need bitwise operators: operators that work on each bit of a number separately.

2. AND, OR, XOR, and NOT

Each operator compares the bits of two numbers position by position. We will use 12 (1100) and 10 (1010).

OperatorNameRule for each bit positionExample
&AND1 only if both bits are 112 & 10 is 8
|OR1 if either bit is 112 | 10 is 14
^XOR1 if the bits are different12 ^ 10 is 6
~NOTflips every bit (one input)~12 is -13

Line them up to see it:

  1100   (12)        1100   (12)        1100   (12)
& 1010   (10)      | 1010   (10)      ^ 1010   (10)
  ----               ----               ----
  1000   (8)         1110   (14)        0110   (6)

In code:

console.log(0b1100 & 0b1010); // 8
console.log(0b1100 | 0b1010); // 14
console.log(0b1100 ^ 0b1010); // 6

Here is how to remember them:

  • AND is a filter: it keeps only bits that are on in both. It is used to check and to clear bits.
  • OR is a merge: it turns bits on. It is used to set bits.
  • XOR is a difference detector: it flips a bit where the other side has a 1. It is used to toggle bits.

~ (NOT) flips every bit. The result looks strange because JavaScript works with signed 32-bit numbers, so flipping 12 gives -13. A handy rule is ~n equals -n - 1. To see just the low bits, combine it with AND:

console.log(~12);               // -13
console.log(~0b1100 & 0b1111);  // 3  (0011: the four bits flipped)

For flags, you will mostly use ~ together with & to clear bits, so you rarely look at the raw negative result.

3. Shifts

A shift slides all the bits left or right.

OperatorMeaningExampleResult
<<Shift left3 << 212
>>Shift right12 >> 23
0011  <<  2  =  1100     (3 becomes 12)
1100  >>  2  =  0011     (12 becomes 3)

Shifting left by 1 doubles the number, shifting left by n multiplies by 2^n. Shifting right by n divides by 2^n and drops the remainder (it rounds toward minus infinity):

console.log(3 << 2);   // 12 (same as 3 * 4)
console.log(1 << 10);  // 1024 (same as 2 ** 10)
console.log(12 >> 2);  // 3  (same as Math.floor(12 / 4))
console.log(13 >> 1);  // 6  (13 / 2 is 6.5, the .5 is dropped)

The shift 1 << n is especially useful. It creates a number with only bit n turned on, counting from zero on the right:

ExpressionBinaryValue
1 << 000011
1 << 100102
1 << 201004
1 << 310008

That gives us the building blocks for flags.

There is also >>>, the unsigned right shift. We will meet it in the 32-bit section.

4. Bitmasks: Packing Many Yes/No Values Into One Number

A flag is a number with exactly one bit set. A bitmask is a number used to select or change certain bits. Let's give each permission its own bit:

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

A user's permissions are one number that combines the flags with OR:

let perms = READ | WRITE;
console.log(perms);                // 6
console.log(perms.toString(2));    // 110

Now the four operations that cover almost every use:

TaskExpressionWhy it works
Check a flag(perms & FLAG) === FLAGAND keeps the flag's bit only if it is on
Set a flagperms | FLAGOR turns the bit on, leaves others alone
Clear a flagperms & ~FLAG~FLAG is all ones except that bit; AND removes it
Toggle a flagperms ^ FLAGXOR flips just that bit
console.log((perms & WRITE) === WRITE); // true
console.log((perms & EXEC) === EXEC);   // false

perms = perms | EXEC;      // set EXEC     -> 111 = 7
perms = perms & ~WRITE;    // clear WRITE  -> 101 = 5
perms = perms ^ WRITE;     // toggle WRITE -> 111 = 7

Why compare with === FLAG and not just test for non-zero? When you check several flags at once, such as READ | WRITE, you need to know that all of them are on. Comparing to the whole mask does that.

5. Unix Permissions and chmod 755

Linux and macOS store file permissions exactly this way. Each file has three groups of three flags:

GroupWho
Owner (user)the person who owns the file
Groupusers in the file's group
Othereveryone else

Each group has read (r = 4), write (w = 2), and execute (x = 1). Add the numbers to get one digit from 0 to 7:

DigitBinaryPermissions
7111rwx
6110rw-
5101r-x
4100r--
0000---

Three digits make the whole mode, which is why you write chmod 755:

chmod 755 deploy.sh   # owner: rwx, group: r-x, other: r-x
ls -l deploy.sh
# -rwxr-xr-x  1 dara  staff  120 Oct  3 09:00 deploy.sh

The 7 is rwx for the owner, the first 5 is r-x for the group, and the last 5 is r-x for everyone else. Common modes:

ModeMeaningTypical use
755rwxr-xr-xprograms, scripts, folders
644rw-r--r--normal files, web pages
600rw-------private keys, secrets

This is why octal is useful: one octal digit is exactly three bits, so 0o755 is 111 101 101 in binary. In JavaScript you can write it directly and pull each group out with a shift and a mask:

const mode = 0o755;
console.log(mode);                // 493
console.log(mode.toString(2));    // 111101101
console.log((mode >> 6) & 0b111); // 7 (owner)
console.log((mode >> 3) & 0b111); // 5 (group)
console.log(mode & 0b111);        // 5 (other)

Each line shifts the group you want down to the lowest three bits, then uses & 0b111 to cut off everything else. We turn this into the function toUnixMode in the recipe.

6. Feature Flags

The same idea works for app settings. Say your product has four optional features and you save them in one number per user:

const Feature = {
  DARK_MODE: 1 << 0, // 1
  BETA: 1 << 1,      // 2
  EXPORT: 1 << 2,    // 4
  ADMIN: 1 << 3,     // 8
};

Using 1 << n means you only have to count positions, not work out powers of two in your head, and it makes it hard to give two flags the same bit by mistake.

let user = Feature.DARK_MODE | Feature.ADMIN;
console.log(user);                       // 9
console.log(user.toString(2).padStart(4, "0")); // 1001

console.log((user & Feature.BETA) === Feature.BETA);   // false
console.log((user & Feature.ADMIN) === Feature.ADMIN); // true

user = user | Feature.BETA;         // 1011
user = user & ~Feature.DARK_MODE;   // 1010
console.log(user);                  // 10

One number stores all four settings. A database column of type integer holds up to 31 or 32 of them. You can compare, copy, and send the number without any parsing.

Honest trade-off: a number like 10 is unreadable on its own, and adding a 33rd flag is not possible in a normal 32-bit column. For settings that people read and edit often, an array such as ["beta", "admin"] or a list of booleans is clearer. Use bit flags when space, speed, or an existing format (like Unix modes or a binary protocol) matters. Chapter 5 shows that a flag number and a set of names are two views of the same idea.

7. Handy Bit Tricks

Once you read bits comfortably, some short expressions become clear:

function isOdd(n) {
  return (n & 1) === 1;      // the lowest bit is 1 for odd numbers
}
console.log(isOdd(7));  // true
console.log(isOdd(10)); // false

function isPowerOfTwo(n) {
  return n > 0 && (n & (n - 1)) === 0;
}
console.log(isPowerOfTwo(64)); // true
console.log(isPowerOfTwo(96)); // false

Why does the power-of-two check work? A power of two has exactly one bit set, like 1000000 (64). Subtracting 1 gives 0111111 (63), where every bit is different, so 64 & 63 is 0. For 96 (1100000) the top bit survives, so the result is not zero.

Another trick is XOR with the same value twice, which gets you back to the start: 7 ^ 2 ^ 2 is 7. Simple encryption puzzles, checksums, and swapping algorithms use it, though in everyday code you will rarely need it.

Be readable first. n % 2 === 0 is clearer than (n & 1) === 0 in most code. Bit tricks are valuable when you are working with flags and binary data, not when you are just trying to look clever.

8. The 32-Bit Caveat in JavaScript

Normal JavaScript numbers are 64-bit floating-point values, as you saw in chapter 2. But bitwise operators work on 32-bit signed integers. Before an operation, JavaScript converts your number to 32 bits, and converts the result back. This causes surprises with big numbers:

console.log(2 ** 31);        // 2147483648
console.log(2 ** 31 | 0);    // -2147483648 (wrapped around to negative)
console.log(1 << 31);        // -2147483648
console.log(1 << 32);        // 1  (the shift count wraps: 32 acts as 0)
console.log(2 ** 32 + 5 | 0); // 5  (the top bits were cut off)
console.log(5.9 | 0);        // 5  (decimals are dropped)

Three rules to take away:

  1. Only 32 bits. Flags above bit 30 are risky. Bit 31 is the sign bit and makes the number negative. Keep flags at 1 << 30 or lower, or at most 31 flags.
  2. Results can be negative. 1 << 31 is -2147483648. Use >>> 0 to read a result as an unsigned (never negative) number:
console.log((1 << 31) >>> 0);   // 2147483648
console.log(-1 >>> 0);          // 4294967295
console.log(~5 >>> 0);          // 4294967290
  1. Need more than 32 flags? Use BigInt, whose operators &, |, ^, << work on numbers of any size. Write BigInt literals with a trailing n:
console.log(1n << 40n);  // 1099511627776n
console.log(5n & 3n);    // 1n

You cannot mix BigInt and normal numbers in one expression, such as 1n | 2, so pick one type per flag set. Chapter 7 covers BigInt properly.

One more habit worth having: x | 0 and ~~x are sometimes used to chop off decimals, but Math.trunc(x) says what you mean and does not break for numbers beyond 32 bits.

9. Recipe: bits.js

Create src/bits.js with permission flags and the four flag operations:

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

export function hasFlag(value, flag) {
  return (value & flag) === flag;
}

export function setFlag(value, flag) {
  return value | flag;
}

export function clearFlag(value, flag) {
  return value & ~flag;
}

export function toggleFlag(value, flag) {
  return value ^ flag;
}

function triple(bits) {
  return (
    (hasFlag(bits, READ) ? "r" : "-") +
    (hasFlag(bits, WRITE) ? "w" : "-") +
    (hasFlag(bits, EXEC) ? "x" : "-")
  );
}

export function toUnixMode(mode) {
  const owner = (mode >> 6) & 0b111;
  const group = (mode >> 3) & 0b111;
  const other = mode & 0b111;
  return triple(owner) + triple(group) + triple(other);
}

triple is a small helper that is not exported: it turns three bits into text like r-x. toUnixMode splits the mode into its three groups, as in section 5, and joins the three pieces.

Add it to src/index.js:

export * from "./bits.js";

(Keep the lines from earlier chapters, including export * from "./numbers.js";.) Then add to demo.js:

import {
  READ, WRITE, EXEC,
  hasFlag, setFlag, clearFlag, toggleFlag, toUnixMode,
} from "./src/index.js";

let perms = READ | WRITE;
console.log(perms);                    // 6
console.log(hasFlag(perms, WRITE));    // true
console.log(hasFlag(perms, EXEC));     // false

perms = setFlag(perms, EXEC);
console.log(perms);                    // 7
perms = clearFlag(perms, WRITE);
console.log(perms);                    // 5
perms = toggleFlag(perms, WRITE);
console.log(perms);                    // 7

console.log(toUnixMode(0o755));        // rwxr-xr-x
console.log(toUnixMode(0o644));        // rw-r--r--
console.log(toUnixMode(0o754));        // rwxr-xr--
console.log(toUnixMode(0o600));        // rw-------

If your demo.js already has an import line from ./src/index.js, merge the names into one import instead of adding a second. Run:

node demo.js

The new lines print 6, true, false, 7, 5, 7, and then the four rwx strings shown in the comments.

10. Try It Yourself

  1. Paper first. Work out 0b1111 & 0b0101, 0b1010 | 0b0101, and 0b1110 ^ 0b0111 by lining up the bits. Check each one in Node with .toString(2).
  2. Add a fourth flag. Add a DELETE flag to your permission set that does not collide with READ, WRITE, or EXEC. Which value should it have? What happens to toUnixMode if you pass it a number containing that flag?
  3. Read a mode. What are chmod 640 and chmod 700 in rwx form? Check your answer with toUnixMode(0o640) and toUnixMode(0o700).
  4. Build the reverse. Write fromUnixMode("rwxr-xr--") that returns the number 0o754. (Hint: each r, w, x is a flag; add READ, WRITE, or EXEC and shift each group into place with <<.)
  5. Feature flag helper. Using the Feature object from section 6, write listFlags(value) that returns the names of all flags that are on, for example ["DARK_MODE", "ADMIN"] for 9. (Hint: Object.entries and hasFlag.)
  6. Break the 32-bit limit. Run 1 << 31, 1 << 32, and (1 << 31) >>> 0. Then rewrite 1 << 40 using BigInt. Explain in your own words why the plain version gives the wrong answer.

You can now store and test many yes/no values in one number. In Boolean Logic, we step back from bits to the true/false rules behind every if statement, and learn how to simplify tangled conditions.

Buy Me a Coffee

Found this tutorial helpful? Consider buying me a coffee to support more free content!