· tips · 6 min read
Bitwise Swapping: A Deeper Dive into JavaScript's Oddities
An in-depth look at swapping variables using the bitwise XOR trick in JavaScript: how it works, why it sometimes bends expectations, practical use-cases, and the pitfalls you must know (ToInt32 conversion, aliasing, typed arrays, BigInt).

Why this odd trick matters
You’ve probably seen the three-line XOR swap in puzzles and old code snippets:
// The classic XOR swap
a = a ^ b;
b = a ^ b;
a = a ^ b;
It looks clever: no temporary variable, just bit-magic. But in JavaScript this trick has several hidden behaviors and traps. In this post we’ll explain why the XOR swap works (in binary), what JavaScript does under the hood, where it breaks, and when - if ever - it’s appropriate to use.
How XOR swapping actually works (bitwise view)
The XOR operator (^) computes bitwise exclusive-or. For bits x and y, x ^ y is 1 when exactly one is 1, otherwise 0. XOR has two properties that enable the swap:
- x ^ x = 0
- x ^ 0 = x
- XOR is commutative and associative: a ^ b == b ^ a
Given two values a and b (assume integers for now):
- a = a ^ b // now a holds (a ^ b)
- b = a ^ b // b = (a ^ b) ^ b = a
- a = a ^ b // a = (a ^ b) ^ a = b
So after the three steps the values are swapped without storing a temporary copy. You can verify this by tracing the bit patterns.
Example trace (8-bit)
Let a = 0101_1010 (0x5A, decimal 90) and b = 0011_0001 (0x31, decimal 49).
- Step 1: a = a ^ b -> a = 0110_1011 (0x6B)
- Step 2: b = a ^ b -> b = 0101_1010 (original a)
- Step 3: a = a ^ b -> a = 0011_0001 (original b)
Both are swapped.
JavaScript specifics: values vs. bits
JavaScript’s arithmetic and bitwise semantics differ from the naive integer model used above:
- Number in JS is a 64-bit IEEE-754 floating point. However, bitwise operators first convert operands to 32-bit signed integers (ToInt32) and the operation runs on 32-bit two’s complement integers. The result is converted back to a JS Number.
- See MDN on bitwise operators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators
- This means any fractional part is truncated, and very large integers outside the 32-bit signed range will be coerced/truncated.
Consequences:
- If you try the XOR-swap on floating numbers, fractions are lost before the swap.
- Very large integers (beyond 32-bit) are truncated and will give unexpected results.
The most important pitfall: aliasing / same location
If the two variables refer to the same storage location, XOR-swap will destroy the value.
Example with an array (swapping two indices):
let arr = [5];
let i = 0,
j = 0; // same index
arr[i] = arr[i] ^ arr[j]; // arr[0] = arr[0] ^ arr[0] -> 0
arr[j] = arr[i] ^ arr[j]; // arr[0] = 0 ^ 0 -> 0
arr[i] = arr[i] ^ arr[j]; // arr[0] = 0 ^ 0 -> 0
// value lost
If i === j you wipe the value to zero. The same conceptual problem exists in lower-level languages when swapping two pointers that alias.
When swapping two distinct variables (like let a = 1, b = 2
), the aliasing problem does not occur because JavaScript variables are independent bindings.
BigInt and alternatives
BigInt supports bitwise operators and does not truncate to 32-bit, so a BigInt XOR swap preserves full integer width (but you must use BigInt values like
1n
,2n
). See MDN BigInt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigIntTyped arrays (Int32Array, Uint32Array) operate on fixed-width 32-bit integers. If you need precise 32-bit behavior and want to manipulate bits reliably, use typed arrays:
let buf = new Int32Array([aValue, bValue]);
buf[0] = buf[0] ^ buf[1];
buf[1] = buf[0] ^ buf[1];
buf[0] = buf[0] ^ buf[1];
But note the aliasing issue if indices are equal still applies to typed arrays.
MDN Int32Array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array
Practical JavaScript examples and safer wrappers
Classic example (works for 32-bit integer-range Numbers)
let a = 42;
let b = 100;
// XOR swap
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a, b); // 100 42
This works, but only because a
and b
are safe 32-bit integer-like values. If they were floats, you’d lose fractions.
A safe JS swap function (preferred in real code)
Most JS code should prefer clarity and correctness over cleverness. The idiomatic ways to swap are:
- Using a temporary variable
let tmp = a;
a = b;
b = tmp;
- Using array destructuring (concise and readable)
[a, b] = [b, a];
Both are crystal-clear and are optimized well by modern engines. Use them unless you have a compelling, measured reason not to.
A defensive XOR-swap helper for typed storage
If you truly need an in-place, allocation-free swap for Int32Array values and want the XOR approach, add guards for aliasing and types:
function xorSwapInt32(arr, i, j) {
if (i === j) return; // guard aliasing
// optional: validate arr is Int32Array and indices are in bounds
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
This protects against the zeroing issue and restricts usage to fixed-width 32-bit integers.
Does it ever make sense performance-wise?
Historically, the XOR swap was used to avoid using extra registers or memory on very constrained CPUs. On modern platforms and high-level languages, it usually does not offer a performance advantage:
- Modern compilers and JavaScript engines (V8, SpiderMonkey, JavaScriptCore) are very good at optimizing simple swaps; they can often fold away temporaries or use registers.
- The XOR swap involves three bitwise operations; a single temporary assignment or destructuring often results in simpler, faster bytecode or JIT’ed code.
StackOverflow discussion on whether XOR swap is useful: https://stackoverflow.com/questions/3413368/why-is-the-xor-swap-algorithm-bad
In short: don’t choose XOR swap for micro-optimizations without measuring in your target environment.
Edge cases and gotchas summary
- Non-integers: bitwise operators coerce to 32-bit signed integers; fractional parts are lost.
- Large integers: greater than 32-bit will be truncated to 32 bits; use BigInt if you need arbitrary integer precision.
- Aliasing: swapping the same location (same array index or same object property) can zero-out the value.
- Clarity: XOR swap is clever but cryptic. For readability and maintainability prefer simple patterns.
When might you use XOR swap in JS?
Realistic scenarios are rare, but a few possibilities:
- You’re manipulating raw Int32Array data in-place, and you need an allocation-free swap inside tight, highly optimized code and you’ve measured benefits.
- Educational/demonstration purposes or interview/puzzle contexts where understanding bitwise algebra is the goal.
Even in these cases, guard against aliasing and the coercion rules described earlier.
Final thoughts
The XOR swap is an elegant bit-manipulation trick with roots in low-level programming. In JavaScript it still “works” under the right conditions (32-bit-compatible integers or BigInts, non-aliased locations), but the language’s type coercions and modern engine optimizations make it a niche tool at best. Favor clarity (destructuring/temp variables) in production code and reach for XOR-swap only when you truly understand the semantics and have a measured need.
References
- MDN - Bitwise operators (JavaScript): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators
- Wikipedia - XOR swap algorithm: https://en.wikipedia.org/wiki/XOR_swap_algorithm
- StackOverflow - Why is the XOR swap algorithm bad?: https://stackoverflow.com/questions/3413368/why-is-the-xor-swap-algorithm-bad
- MDN - Int32Array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array
- MDN - BigInt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt