Testing dates before 1970 in JavaScript

JavaScript dates reach 271,821 years before 1970, yet a lot of code quietly assumes timestamps are positive. One negative timestamp with a fraction of a second is usually enough to show it.

Specimens

8

dates(), 3 tags

Reading time

2 min

5 sections

Updated

2026-10-13

edgegoat 1.0.0

The bug: negative milliseconds round toward zero

new Date(-1500) is 1969-12-31T23:59:58.500Z, one and a half seconds before the epoch. Math.trunc(-1.5) is -1, not -2, and -1500 % 1000 is -500, not 500. Code that splits a timestamp into seconds and a fraction with truncation and % gets 23:59:59 and a negative fraction.

A naive implementation that fails

Protocol Buffers and many APIs encode time as { seconds, nanos } where nanos must be between 0 and 999,999,999:

example.js
function toTimestamp(date) {
  const ms = date.getTime();
  return { seconds: Math.trunc(ms / 1000), nanos: (ms % 1000) * 1e6 };
}

toTimestamp(new Date(-1500)); // { seconds: -1, nanos: -500000000 }  invalid

The edgegoat test that catches it

example.test.js
import { expect, test } from 'vitest';
import { dates } from 'edgegoat';

test.each(dates({ tags: ['pre-1970', 'epoch'] }))('$label', ({ value }) => {
  const { seconds, nanos } = toTimestamp(value);
  expect(nanos).toBeGreaterThanOrEqual(0);
  expect(nanos).toBeLessThan(1e9);
  expect(seconds * 1000 + nanos / 1e6).toBe(value.getTime());
});

Fails on

1 ms before the epoch, before 1970 with fractional seconds and moon landing 1969-07-20T20:17:40.123Z. The fix floors: const seconds = Math.floor(ms / 1000) and nanos = (ms - seconds * 1000) * 1e6.

Local mean time before time zones existed

Before standard time zones were adopted, each city kept its own local mean time. The tz database records offsets with seconds, such as -03:06:28 for Sao Paulo before 1914. getTimezoneOffset() reports whole minutes, so local arithmetic on old dates can drift by seconds. The lmt case in dates() covers it.

Year 0, negative years and the far edges

dates() also includes year 0, year -1 (-000001 in ISO format), the minimum and maximum valid dates (plus and minus 8.64e15 ms), year 9999 and year 10000, where toISOString() switches to a six digit, signed year.

Note

new Date(0, 0, 1) is 1900, not year 0: the Date constructor maps years 0 to 99 to 1900 to 1999. Use setFullYear to build early years.

FAQ

Can JavaScript Date represent dates before 1970?

Yes. A Date stores milliseconds since 1970-01-01T00:00:00Z as a signed number, so earlier dates are negative. The valid range is -271821-04-20 to +275760-09-13.

Why is my pre-1970 timestamp off by one second?

Truncation rounds negative values toward zero. Use Math.floor for the seconds and compute the remainder from it, so the fraction stays positive.

Why does a date in 1850 have a strange UTC offset?

Before standard time, zones used local mean time, with offsets such as -03:06:28. Intl reports them exactly, getTimezoneOffset() in whole minutes.

Does new Date(0, 0, 1) give year 0?

No. Years 0 to 99 passed to the Date constructor or Date.UTC become 1900 to 1999. Call setFullYear(0, 0, 1) instead.

More lab notes