You will need to subtract the time zone offset of your local time zone from the Date instance, before you pass it to format from date-fns. For example:

const dt = new Date('2017-12-12');
const dtDateOnly = new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000);
console.log(format(dtDateOnly, 'YYYY-MM-DD')); // Always "2017-12-12"

Problem

You want to handle only the date part of the Date instance, because the time part does not make sense for birthdates. However, the Date object does not offer any "date-only" mode. You can access both its date and time parts in the local time zone or UTC. The problem is, that format from date-fns prints the output always in the local time zone.

When you executed the constructor only with the date part:

const dt = new Date('2017-12-12');

The JavaScript engine actually assumed a string in the incomplete ISO 8601 format and perfomed this:

const dt = new Date('2017-12-12T00:00:00.000Z');

It may still look "harmless" to you, but the date instance exposes the value not only in UTC, but also in the local time zone. If you construct the Date instance on the East Coast of the US, you will see the following output:

> const dt = new Date('2017-12-12');
> dt.toISOString()
'2017-12-12T00:00:00.000Z'
> dt.toString()
'Tue Dec 11 2017 19:00:00 GMT-0500 (EST)'
> d.toLocaleString()
'12/11/2017 7:00:00 PM'

Solution

If you know, that format from date-fns reads date and time parts from the date instance in the local time zone, you will need to make your date "looking like" the midnight in your local time zone and not in UTC, which you passed to the Date constructor. Then you will see the year, month and date numbers preserved. It means, that you need to subtract the time zone offset of your local time zone for the specified day. Date.prototype.getTimezoneOffset returns the offset, but with an inverted sign and in minutes.

const dt = new Date('2017-12-12');
// Tue Dec 11 2017 19:00:00 GMT-0500 (EST)
const dtDateOnly = new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000);
// Tue Dec 12 2017 00:00:00 GMT-0500 (EST)
console.log(format(dtDateOnly, 'YYYY-MM-DD'));
// Prints always "2017-12-12", regardless the time zone it executed in

However, such Date instance can be used only to format the date-only value. You cannot use it for computing date differences, for example, which would need the original and correct UTC value.

Alternative

If you need always the same date-only format and not the format specific to the current locale, you do not need date-fns. You can format the string by the concatenation of padded numbers:

const dt = new Date('2017-12-12');

const year = dt.getUTCFullYear()
const month = dt.getUTCMonth() + 1 // Date provides month index; not month number
const day = dt.getUTCDate()

// Print always "2017-12-12", regardless the time zone it executed in
console.log(year + '-' + padToTwo(month) + '-', padToTwo(day));
// Or use a template literal
console.log(`${year}-${padToTwo(month)}-${padToTwo(day)}`);

function padToTwo (number) {
  return number > 9 ? number : '0' + number
}
Answer from Ferdinand Prantl on Stack Overflow
Top answer
1 of 2
79

You will need to subtract the time zone offset of your local time zone from the Date instance, before you pass it to format from date-fns. For example:

const dt = new Date('2017-12-12');
const dtDateOnly = new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000);
console.log(format(dtDateOnly, 'YYYY-MM-DD')); // Always "2017-12-12"

Problem

You want to handle only the date part of the Date instance, because the time part does not make sense for birthdates. However, the Date object does not offer any "date-only" mode. You can access both its date and time parts in the local time zone or UTC. The problem is, that format from date-fns prints the output always in the local time zone.

When you executed the constructor only with the date part:

const dt = new Date('2017-12-12');

The JavaScript engine actually assumed a string in the incomplete ISO 8601 format and perfomed this:

const dt = new Date('2017-12-12T00:00:00.000Z');

It may still look "harmless" to you, but the date instance exposes the value not only in UTC, but also in the local time zone. If you construct the Date instance on the East Coast of the US, you will see the following output:

> const dt = new Date('2017-12-12');
> dt.toISOString()
'2017-12-12T00:00:00.000Z'
> dt.toString()
'Tue Dec 11 2017 19:00:00 GMT-0500 (EST)'
> d.toLocaleString()
'12/11/2017 7:00:00 PM'

Solution

If you know, that format from date-fns reads date and time parts from the date instance in the local time zone, you will need to make your date "looking like" the midnight in your local time zone and not in UTC, which you passed to the Date constructor. Then you will see the year, month and date numbers preserved. It means, that you need to subtract the time zone offset of your local time zone for the specified day. Date.prototype.getTimezoneOffset returns the offset, but with an inverted sign and in minutes.

const dt = new Date('2017-12-12');
// Tue Dec 11 2017 19:00:00 GMT-0500 (EST)
const dtDateOnly = new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000);
// Tue Dec 12 2017 00:00:00 GMT-0500 (EST)
console.log(format(dtDateOnly, 'YYYY-MM-DD'));
// Prints always "2017-12-12", regardless the time zone it executed in

However, such Date instance can be used only to format the date-only value. You cannot use it for computing date differences, for example, which would need the original and correct UTC value.

Alternative

If you need always the same date-only format and not the format specific to the current locale, you do not need date-fns. You can format the string by the concatenation of padded numbers:

const dt = new Date('2017-12-12');

const year = dt.getUTCFullYear()
const month = dt.getUTCMonth() + 1 // Date provides month index; not month number
const day = dt.getUTCDate()

// Print always "2017-12-12", regardless the time zone it executed in
console.log(year + '-' + padToTwo(month) + '-', padToTwo(day));
// Or use a template literal
console.log(`${year}-${padToTwo(month)}-${padToTwo(day)}`);

function padToTwo (number) {
  return number > 9 ? number : '0' + number
}
2 of 2
12

Only adding the @ferdinand-prantl answer. If you are using the date-fns, you can parse the string date ('2017-12-12') using the parseISO(here) fn from date-fns, which will complete the missing ISO 8601 format with your local time zone. When you use the format fn, you are going to keep the date.

const strDate = '2017-12-12';
const isoDate = parseISO(strDate);
const formattedDate = format(isoDate, 'YYYY-MM-DD');
console.log({strDate, isoDate, formattedDate})
//{
//  strDate: '2017-12-12',
//  isoDate: 2017-12-12T02:00:00.000Z,
//  formattedDate: '2017-12-12'
//}
🌐
GitHub
github.com › date-fns › date-fns › issues › 489
Specify a time zone for format()? · Issue #489 · date-fns/date-fns
May 18, 2017 - I recently encountered a phenomenon in date-fns that broke a Node.js script of mine: const d = new Date('2017-01-23'); console.log(format(d, 'YYYY-MM-DD')); This code logs 2017-01-23 in Germany, but 2017-01-22 in Seattle. How do I best fix this to always log 2017-01-23?
Published   May 18, 2017
🌐
Mastering JS
masteringjs.io › tutorials › date-fns › tz
Working with Timezones using date-fns and date-fns-tz - Mastering JS
Instead of using utcToZonedTime(), store the time and the timezone you want to display, and use formatInTimeZone(). While we often use date-fns for convenience, you don't need date-fns to do basic tasks like formatting dates in common timezones.
🌐
GitHub
github.com › date-fns › date-fns › issues › 2151
formatISO doesn't use UTC (Z) timezone · Issue #2151 · date-fns/date-fns
January 12, 2021 - When making a sanity check of now.toISOString() === formatISO(now) (with const now = new Date()), it fails due to a timezone difference, as the formatISO doesn't match the browser's toISOString, which explicitly claims that The timezone is always zero UTC offset, as denoted by the suffix "Z"..
Published   Jan 12, 2021
🌐
date-fns
date-fns.org › docs
date-fns - modern JavaScript date utility library
date-fns provides the most comprehensive yet simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js.
🌐
npm
npmjs.com › package › date-fns-tz
date-fns-tz - npm
The zzzz Unicode token: long specific ... date-fns/format, the z..zzzz, x..xxxxx, X..XXXXX and O..OOO tokens will all print the formatted value of the provided time zone rather than the system time zone....
      » npm install date-fns-tz
    
Published   Sep 30, 2024
Version   3.2.0
Author   Marnus Weststrate
Top answer
1 of 12
73

UPDATE: date-fns v4 finally has builtin TZ support

I haven't tried it.

UPDATE: For date-fns v3, use date-fns-tz's formatInTimeZone()

A formatInTimeZone() helper has been added several years ago, but some bugs were fixed recently, best use at least v3.1.2.

<script type="importmap">{ "imports": { "date-fns": "https://esm.run/[email protected]", "date-fns/": "https://esm.run/[email protected]/", "date-fns-tz": "https://esm.run/[email protected]" } }</script>
<script type="module">

import { parseISO } from 'date-fns';
import { enGB } from 'date-fns/locale/en-GB';
import { fr   } from 'date-fns/locale/fr';
import { formatInTimeZone } from 'date-fns-tz';

const parsedTime = parseISO("2019-10-25T07:10:00Z");
console.log('parsedTime:', parsedTime);


const formattedTime = formatInTimeZone(
  parsedTime, "UTC", "yyyy-MM-dd kk:mm:ss zzzz", 
  // IF using 'zzzz', can optionally pass locale for better TZ names
  { locale: fr });
console.log('formattedTime:', formattedTime); // 2019-10-25 07:10:00 temps universel coordonné
</script>

The implementation is quite similar to the following (but now it also compensates for shifted time falling on wrong side of DST — not an issue for UTC but can be for many TZs):

Previous "official hack"

You were almost there, but format() only uses timeZone to show TZ name; you also need to shift the time correspondingly. Here is the combo date-fns-tz used to recommend:

<script type="importmap">{ "imports": { "date-fns": "https://esm.run/[email protected]", "date-fns/": "https://esm.run/[email protected]/", "date-fns-tz": "https://esm.run/[email protected]" } }</script>
<script type="module">

import { parseISO } from 'date-fns';
import { enGB } from 'date-fns/locale/en-GB';
import { fr   } from 'date-fns/locale/fr';
import { format, toZonedTime } from 'date-fns-tz';

const time = "2019-10-25T08:10:00+01:00"; // = 07:10Z

const parsedTime = parseISO("2019-10-25T07:10:00Z");
console.log('parsedTime:', parsedTime);


const formatInTimeZone = (date, timeZone, fmt, locale) =>
  // called `toZonedTime` since 3.0.0, was `utcToZonedTime` before.
  format(toZonedTime(date, timeZone), 
         fmt, 
         // timeZone required! locale is optional, helps 'zzzz' names.
         { timeZone, locale });

const formattedTime = formatInTimeZone(parsedTime, "UTC", "yyyy-MM-dd kk:mm:ss zzzz", fr);
console.log('formattedTime:', formattedTime); // 2019-10-25 07:10:00 temps universel coordonné
</script>

Behind the scenes

The date-fns[-tz] libraries stick to the built-in Date data type that carries no TZ info.
Some functions treat it as a moment-in-time, but some like format treat it more like a struct of calendaric components — year 2019, ..., day 25, hour 08, ....

Now the trouble is a Date is internally only a moment in time. Its methods provide a mapping to/from calendaric components in local time zone.

So to represent a different time zone, date-fns-tz/utcToZonedTime temporarily produces Date instances which represent the wrong moment in time — just to get its calendaric components in local time to be what we want!

And the date-fns-tz/format function's timeZone input affects only the template chars that print the time zone (XX..X, xx..x, zz..z, OO..O).

See https://github.com/marnusw/date-fns-tz/issues/36 for some discussion of this "shifting" technique (and of real use cases that motivated them)...
It's a bit low-level & risky, but the specific way I composed them above — formatInTimeZone() — is I believe a safe recipe? UPDATE: except TZ name could be wrong around DST boundary !

2 of 12
41

I would suggest using the built-in Date util:

const date = new Date("2019-10-25T08:10:00Z");
const isoDate = date.toISOString();

console.log(`${isoDate.substring(0, 10)} ${isoDate.substring(11, 19)}`);

Outputs:

2019-10-25 08:10:00

Not a general solution for any format, but no external libraries required.

Find elsewhere
🌐
Restack
restack.io › p › best-open-source-time-tracking-software-answer-javascript-date-without-timezone
Javascript Date Without Timezone | Restackio
Date Libraries: While native JavaScript provides basic date handling, libraries like date-fns can simplify operations. For example, date-fns allows you to format dates without worrying about time zones.
🌐
Stack Overflow
stackoverflow.com › questions › 72931061 › how-to-format-date-string-with-date-fns-without-timezone-transformation
How to format date string with date-fns without timezone transformation? - Stack Overflow
... const getRawDate = (rawDate) => { if (!isValid(new Date(rawDate))) { return null; } // Split by T separator in order to take date and time const [date, time] = rawDate.split('T'); // return date and time without timezone ...
🌐
LogSnag
logsnag.com › blog › handling-timezones-in-javascript-with-date-fns-tz
How to Handle Time-Zones in JavaScript with date-fns-tz | LogSnag
October 20, 2023 - Since all the employees around ... to format the date of the party in a specific time zone. date-fns-tz provides us with a function called formatInTimeZone for this....
🌐
GitHub
github.com › date-fns › tz
GitHub - date-fns/tz: date-fns timezone utils · GitHub
Using it makes date-fns operate in given time zone but can also be used without it.
Author   date-fns
🌐
Stack Overflow
stackoverflow.com › questions › 74483578 › how-do-i-format-a-date-object-with-no-time-using-date-fns-and-retaining-utc
How do I format a date object with no time using date-fns and retaining UTC?
Either hack the timezone or use date-fns-tz extension. stackoverflow.com/a/52352512/1231844 ... @OFRBG I was afraid someone was going to say something like "hack the date-fns lib".
🌐
Date-fns
blog.date-fns.org › v40-with-time-zone-support
v4.0 is out with first-class time zones support!
In 2022, I came up with @date-fns/utc that immediately felt right. You get an option to make all the calculations in UTC and remove the local time zone from the equation without adding any extra weight to the core library.
🌐
GitHub
github.com › marnusw › date-fns-tz
GitHub - marnusw/date-fns-tz: Complementary library for date-fns v2 adding IANA time zone support · GitHub
Since a JavaScript Date instance cannot convey the time zone information to the format function it is necessary to pass the timeZone value as an option on the third argument of format.
Author   marnusw
🌐
Date-fns
blog.date-fns.org › v3-is-out
v3 is out!
My reasoning for the removal was ... widespread issue was passing the date without time and getting UTC midnight instead of the local timezone, causing bugs in the user code. In both cases, the developers would blame date-fns, putting the support burden on us....
🌐
GitHub
github.com › marnusw › date-fns-tz › issues › 55
Format method ignores the timeZone option · Issue #55 · marnusw/date-fns-tz
March 6, 2020 - I'm getting unexpected output when using format method. this is valid for v2.10 consider this example: import format from "date-fns-tz/format"; import parseISO from "date-fns/paseISO"; function getTimeString(dateInput: string): string { ...
Published   Mar 06, 2020
🌐
Reddit
reddit.com › r/javascript › [askjs] i replaced date-fns library with moment-timezone library.
r/javascript on Reddit: [AskJS] I replaced date-fns library with moment-timezone library.
May 12, 2023 -

My country has restored Daylight Saving Time after a long pause. My app now is not working properly like before because of the DST.

Currently, I am using date-fns and date-fns-tz to handle the zoned scheduled events.

I have just tried some libraries like luxon and moment-timezone to figure out if they actually consider the Daylight Saving Time.

I found that luxon does not consider it, and, moment-timezone considers it properly.

Perhaps, all these libraries are considering the DST, but for my country, they don't.

What is your opinion regarding this situation? Should I replace the date-fns ? and if not so, what should I do to make date-fns to consider Daylight Saving Time?

🌐
Npm
npm.io › package › date-fns-tz
Date-fns-tz NPM | npm.io
The zzzz Unicode token: long specific non-location format, e.g. Eastern Standard Time · Unlike date-fns/format, the z..zzzz, x..xxxxx, X..XXXXX and O..OOO tokens will all print the formatted value of the provided time zone rather than the system time zone.