skip to content

In JavaScript, why does new Date() + 1 produce a string while new Date() - 1 produces a number?

level: middleimportance: should knowfreq 40%

answer

  1. one built-in bends the default hint
  2. plus and minus ask different questions
  3. subtraction wants a timestamp
  4. concatenation wants readable text
  5. unary plus forces the number path

basics

~20 s

Date is the one built-in whose Symbol.toPrimitive treats the "default" hint as "string". Binary + passes the default hint and gets the date's text form, so it concatenates; subtraction passes the "number" hint and gets the millisecond timestamp, so it subtracts.

solid answer

~40 s

Binary `+` converts its operands with the **default** hint, while `-` uses the **number** hint. `Date.prototype[Symbol.toPrimitive]` is the only standard implementation that distinguishes the two: for `"string"` *and* `"default"` it produces `toString()` output — the long human-readable date — and only for `"number"` does it produce `valueOf()`, the epoch milliseconds. So `new Date() + 1` concatenates `'Wed Aug 19 2026 ... 1'`, while `new Date() - 1` is a plain number. This is exactly why `date2 - date1` is the idiomatic way to get an elapsed duration in milliseconds and `date2 + date1` is meaningless. If you want the numeric value from a `+`-shaped expression, force the number hint first with unary `+date`, `date.getTime()`, or `Number(date)`.

code

javascript · 11 lines
javascript
const d = new Date(0);

console.log(typeof (d + 1));   // 'string'  default hint → toString
console.log(typeof (d - 1));   // 'number'  number hint → valueOf
console.log(+d);               // 0
console.log(d.getTime());      // 0

const a = new Date(0), b = new Date(0);
console.log(a <= b);           // true  — timestamps compared
console.log(a === b);          // false — distinct objects
console.log(a.getTime() === b.getTime()); // true

go deeper

for a junior

Know that subtracting two dates gives milliseconds and adding them gives nonsense text, and that getTime() is the explicit way to reach the number.

for a middle

Explain the mechanism: + passes the default hint, - passes the number hint, and Date's Symbol.toPrimitive is the one standard implementation that maps default onto the string path.

for a senior

Point at the operational consequence — a host-dependent date string leaking into a cache key, id or log field — and state the rule you enforce: explicit getTime() or toISOString() at every serialisation boundary.

for a principal

Take the API-design position: implicit conversions that differ per operator are a maintenance hazard, so domain time types should expose named conversions and, if they implement Symbol.toPrimitive at all, decide consciously what the default hint means.

## The hint that only Date treats specially When JavaScript needs a primitive from an object it calls ToPrimitive with a hint of `"string"`, `"number"`, or `"default"`. For ordinary objects `"default"` and `"number"` behave identically — both try `valueOf()` first and fall back to `toString()`. `Date` is the deliberate exception. Its `Symbol.toPrimitive` method is specified to pick the string path when the hint is `"string"` **or** `"default"`, and the number path only for `"number"`. So the answer hangs entirely on which hint each operator passes: - binary `+` → `"default"` → Date gives its `toString()` text → the other operand is stringified and concatenated - `-`, `*`, `/`, `<`, `>`, unary `+`, `Number()` → `"number"` → Date gives `valueOf()`, the epoch milliseconds - `String()`, template interpolation → `"string"` → the `toString()` text ```js const d = new Date(0); d + 1; // 'Thu Jan 01 1970 ... 1' (host time zone; string branch) d - 1; // -1 number branch: 0 - 1 +d; // 0 unary + is the number hint Number(d); // 0 String(d); // 'Thu Jan 01 1970 ...' `${d}`; // same text as String(d) d.getTime(); // 0 explicit, no coercion involved ``` ## Why the language made this choice Historically `Date` predates `Symbol.toPrimitive` and its `valueOf` already returned a timestamp. Had `+` used the number hint, `date + ''` would have produced a bare millisecond count, which is almost never what someone concatenating a date wants. Biasing the default hint towards the readable form makes `'Meeting at ' + date` do the useful thing, at the cost of the asymmetry you are asked about in interviews. ## Consequences you actually meet **Durations work, sums do not.** `end - start` is idiomatic and returns milliseconds because subtraction forces the number hint on both operands. There is no meaningful `+` counterpart — adding two dates concatenates two long strings. ```js const start = new Date(); // ... work ... const elapsedMs = new Date() - start; // number ``` **Comparison works, equality does not.** Relational operators use the number hint, so `d1 < d2` compares timestamps correctly. But `==` and `===` between two `Date` objects compare object identity, not time: `===` never coerces at all, and `==` between two objects also compares references. Two distinct `Date` objects for the same instant are never `==` to each other; compare `d1.getTime() === d2.getTime()` (or `+d1 === +d2`) instead. ```js const a = new Date(0), b = new Date(0); a < b; // false — timestamps compared a <= b; // true a === b; // false — different objects a.getTime() === b.getTime(); // true ``` The `a <= b` / `a === b` pair is a nice illustration: `<=` coerces and `===` does not, so a date can be simultaneously "less than or equal" and "not equal" to another. **Accidental string keys.** Because the default hint stringifies, a `Date` used where a number was expected can silently become long text — in a concatenated cache key, an id built with `+`, or a value appended into a query string. The symptom is a key containing `'Wed Aug 19 2026 14:03:11 GMT+0200 (Central European Summer Time)'`, which also varies with the host time zone and locale. Convert explicitly with `getTime()` or `toISOString()` when a stable serialisation matters. ## Making the intent explicit Relying on hints is clever but unreadable. In code that others maintain, prefer the explicit form for whichever value you want: ```js const ms = date.getTime(); // or Number(date), +date const text = date.toISOString(); // stable, time-zone-independent ``` If a class of your own carries a timestamp and is likely to be concatenated, you can copy Date's trick: implement `Symbol.toPrimitive(hint)` and branch on the hint yourself, deciding deliberately what `"default"` should mean rather than inheriting the surprise.

  • How do you check that two Date objects represent the same instant?
    Compare their numeric values, not the objects: `d1.getTime() === d2.getTime()`, or the terser `+d1 === +d2`. `===` on the objects compares references and is false for two separately constructed dates, and `==` between two objects also compares references, so neither equality operator ever coerces here. Relational operators like `<=` do coerce, which is why `a <= b && a >= b` can be true while `a === b` is false.
  • What is the practical risk of letting a Date coerce to a string implicitly?
    `Date.prototype.toString` is host- and time-zone-dependent, so the same instant yields different text on different machines. If that text leaks into a cache key, an id, a sort key or a log field you get values that do not match across environments. Use `toISOString()` for a stable, UTC-anchored representation, or `getTime()` when a number is wanted.

saying these in an interview costs you the question

  • Says + and - always coerce a Date the same way
  • Claims valueOf returns the formatted date string
  • Thinks === on two Dates compares the instants
  • Believes every object treats default and number hints differently
  • Uses date1 + date2 expecting a numeric sum

context