JavaScript Basics: Work with UTC Dates and Timestamps
A timestamp identifies an instant, while a formatted date presents it to a reader. This tutorial creates UTC dates, calculates elapsed time, and checks the difference between an invalid date and a normalized one.
All examples use built-in features and were tested on Linux with Node.js v24.19.0. Assertions stop the program if a result differs from the expected value. A successful run prints the output shown below. Fixed inputs and UTC output make the results independent of your computer’s local time zone.
Prepare a scratch directory
You need Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. Save source files as UTF-8; their .mjs extension selects ES modules. Run these commands in Bash and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Create each named file with your editor. No package installation is needed.
1. Create the same instant two ways
Save this as utc-instant.mjs:
import assert from 'node:assert/strict';
const parsedInstant = new Date('2026-10-17T09:30:00Z');
const constructedInstant = new Date(Date.UTC(2026, 9, 17, 9, 30));
assert.equal(parsedInstant.getTime(), constructedInstant.getTime());
assert.equal(parsedInstant.getUTCFullYear(), 2026);
assert.equal(parsedInstant.getUTCMonth(), 9);
assert.equal(parsedInstant.getUTCDate(), 17);
assert.equal(parsedInstant.getUTCHours(), 9);
assert.equal(parsedInstant.toISOString(), '2026-10-17T09:30:00.000Z');
console.log(`Parsed instant: ${parsedInstant.toISOString()}`);
console.log(`Constructed instant: ${constructedInstant.toISOString()}`);
console.log(`UTC month index: ${parsedInstant.getUTCMonth()}`);Run it:
node utc-instant.mjsOutput:
Parsed instant: 2026-10-17T09:30:00.000Z
Constructed instant: 2026-10-17T09:30:00.000Z
UTC month index: 9The Z in the input means UTC. Date.UTC() accepts UTC components and returns a timestamp in milliseconds; new Date(timestamp) wraps it in a date object. Months are zero-based, so October is 9. Years from 0 through 99 have a legacy mapping to 1900 through 1999.
toISOString() formats an instant in UTC. Prefer an explicit Z or numeric offset in date-time strings. A date-time string without an offset is interpreted as local time, while a date-only string has different UTC parsing rules. Avoid ambiguous formats such as 10/17/2026 for interchange.
2. Calculate an elapsed duration
Save this as elapsed-time.mjs:
import assert from 'node:assert/strict';
const startInstant = new Date('2026-10-17T09:30:00Z');
const finishInstant = new Date('2026-10-17T11:00:00Z');
const millisecondsPerMinute = 60 * 1000;
const elapsedMilliseconds = finishInstant.getTime() - startInstant.getTime();
const elapsedMinutes = elapsedMilliseconds / millisecondsPerMinute;
const reminderInstant = new Date(
startInstant.getTime() + 15 * millisecondsPerMinute,
);
assert.equal(elapsedMilliseconds, 5400000);
assert.equal(elapsedMinutes, 90);
assert.equal(reminderInstant.toISOString(), '2026-10-17T09:45:00.000Z');
assert.equal(startInstant.toISOString(), '2026-10-17T09:30:00.000Z');
console.log(`Elapsed milliseconds: ${elapsedMilliseconds}`);
console.log(`Elapsed minutes: ${elapsedMinutes}`);
console.log(`Reminder: ${reminderInstant.toISOString()}`);Run it:
node elapsed-time.mjsOutput:
Elapsed milliseconds: 5400000
Elapsed minutes: 90
Reminder: 2026-10-17T09:45:00.000ZA Date timestamp is measured in milliseconds from the Unix epoch. Subtract timestamps for an elapsed duration. Constructing a new date for the reminder leaves the starting object unchanged. Adding a fixed number of milliseconds means elapsed time; it is not a general solution for “the same local time tomorrow,” because daylight-saving transitions can change local day lengths. A Date also does not retain the input’s original time-zone label.
3. Distinguish invalid input from normalization
Save this as date-caveats.mjs:
import assert from 'node:assert/strict';
const invalidInstant = new Date('not-a-date');
assert.equal(Number.isNaN(invalidInstant.getTime()), true);
assert.throws(() => invalidInstant.toISOString(), RangeError);
// February has 28 days in 2026. The extra days roll into March.
const normalizedInstant = new Date(Date.UTC(2026, 1, 30));
assert.equal(Number.isNaN(normalizedInstant.getTime()), false);
assert.equal(normalizedInstant.toISOString(), '2026-03-02T00:00:00.000Z');
assert.notEqual(normalizedInstant.getUTCMonth(), 1);
console.log(`Invalid timestamp: ${Number.isNaN(invalidInstant.getTime())}`);
console.log(`Normalized date: ${normalizedInstant.toISOString()}`);
console.log('Invalid ISO formatting: RangeError');Run it:
node date-caveats.mjsOutput:
Invalid timestamp: true
Normalized date: 2026-03-02T00:00:00.000Z
Invalid ISO formatting: RangeErrorAn invalid date has a NaN timestamp, and ISO formatting it throws. However, Date.UTC() normalizes out-of-range components, so a finite timestamp does not prove that the supplied calendar date existed. For strict component input, validate ranges and compare the resulting UTC year, month, and day with the requested values. Do not rely on parsing impossible date strings to reject them consistently across implementations.
Clean up
In the same terminal, remove only the files created here, return to your original directory, and remove the now-empty scratch directory:
rm -- utc-instant.mjs elapsed-time.mjs date-caveats.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.