jsguides

Set.prototype.symmetricDifference()

symmetricDifference(other)

Set.prototype.symmetricDifference() returns a new Set containing every element that is present in the receiver or the argument, but not in both. It is the set-theoretic XOR operation, written A ⊖ B = (A \ B) ∪ (B \ A).

The method ships in the proposal-set-methods bundle and reached Stage 4 for ES2025.

Syntax

setInstance.symmetricDifference(other)

Parameters

  • other — a Set, or any set-like object exposing a keys() method that yields an iterator of values. Arrays are not set-like on their own; wrap them with new Set(arr) first.

Return value

A new Set containing every element that is in exactly one of the two operands. Neither the receiver, the argument, nor any nested objects are modified.

Basic usage

The classic example: even numbers and perfect squares share 4, so it gets dropped from the result.

const evens = new Set([2, 4, 6, 8]);
const squares = new Set([1, 4, 9]);

console.log(evens.symmetricDifference(squares));
// Set(5) { 2, 6, 8, 1, 9 }

The overlap (4) is excluded. Every other element survives, with the receiver’s values listed before the values that come from the argument.

Overlapping and disjoint operands

The shape of the result depends on how the operands relate:

RelationshipResult
Disjoint setsEquivalent to union()
Equal setsEmpty Set
a is a subset of bThe elements unique to b
b is a subset of aThe elements unique to a
Partial overlapElements that appear in only one operand
const a = new Set([1, 2, 3]);
const b = new Set([1, 2, 3]);

console.log(a.symmetricDifference(b));
// Set(0) {}

const c = new Set([1, 2, 3]);
const d = new Set([4, 5, 6]);

console.log(c.symmetricDifference(d));
// Set(6) { 1, 2, 3, 4, 5, 6 }

const e = new Set([1, 2, 3]);
const f = new Set([1, 2, 3, 4, 5]);

console.log(e.symmetricDifference(f));
// Set(2) { 4, 5 }

A useful identity to keep in mind: a.symmetricDifference(b) produces the same membership as a.union(b).difference(a.intersection(b)). Same elements, same order.

Order of the returned Set

The result keeps the receiver’s iteration order first, then appends any values from other that were not already in the receiver, in the order other.keys() yields them. This is the same ordering rule used by Set.prototype.union() and Set.prototype.difference().

const a = new Set(['a', 'b', 'c']);
const b = new Set(['b', 'c', 'd', 'e']);

console.log(a.symmetricDifference(b));
// Set(3) { 'a', 'd', 'e' }

'b' and 'c' (the shared values) are dropped. 'a' stays in receiver order; 'd' and 'e' append in the order b yields them.

Equality and the NaN surprise

Set.prototype.symmetricDifference() uses SameValueZero equality, the same rule Set already uses for membership. Two consequences worth knowing:

  • NaN is treated as equal to NaN.
  • +0 and -0 collapse to a single value.
const a = new Set([NaN, 1, 2]);
const b = new Set([NaN, 3]);

console.log(a.symmetricDifference(b));
// Set(3) { 1, 2, 3 }

A ===-based mental model would predict the opposite (both NaN “not equal”). SameValueZero lines up with the rest of Set.

The receiver must be a real Set

Unlike the argument, the receiver is not set-like-friendly. The spec reads an internal slot from the receiver, so calling the method on a duck-typed object throws:

const fakeSet = {
  size: 2,
  has() { return false; },
  keys() { return [].values(); },
};

try {
  fakeSet.symmetricDifference(new Set());
} catch (err) {
  console.log(err.constructor.name);
  // TypeError
}

The argument side is the opposite story. It only needs a keys() iterator — any object with a working iterator that yields values is enough, no Set constructor required. The reason sits in the spec: the receiver reads an internal slot that duck-typed objects simply do not have, but the argument is iterated by hand, so the call site gets to decide what counts as set-like.

const receiver = new Set([1, 2, 3]);

const setLike = {
  size: 2,
  has(x) { return x === 4 || x === 5; },
  keys() {
    return [4, 5][Symbol.iterator]();
  },
};

console.log(receiver.symmetricDifference(setLike));
// Set(5) { 1, 2, 3, 4, 5 }

That asymmetry (strict receiver, lenient argument) is the part that catches people off guard.

Chaining with the other ES2025 set methods

All the new ES2025 set methods return fresh Set instances, so they compose cleanly:

const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const c = new Set([4, 5, 6, 7]);

console.log(a.symmetricDifference(b).difference(c));
// Set(3) { 1, 2, 3 }

This reads as (a ⊖ b) \ c: the elements that were unique to a or b, and also not in c. Each call allocates a new Set, so the original operands stay untouched. The originals never move.

Manual fallback for older runtimes

If you need to support Node < 22, Safari < 17, or older engines, a two-loop implementation does the job:

function symmetricDifference(a, b) {
  const out = new Set();
  for (const v of a) if (!b.has(v)) out.add(v);
  for (const v of b) if (!a.has(v)) out.add(v);
  return out;
}

const evens = new Set([2, 4, 6, 8]);
const squares = new Set([1, 4, 9]);
console.log(symmetricDifference(evens, squares));
// Set(5) { 2, 6, 8, 1, 9 }

For production use, core-js and the spec-faithful es-shims/set.prototype.symmetricdifference polyfill are the easier options.

Browser and runtime support

The method reached Baseline 2024 in June 2024.

EngineFirst version
Chrome122
Edge122
Firefox127
Safari17.0
Node.js22.0.0

Older runtimes and IE 11 need a polyfill.

See also