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
🌐
date-fns
date-fns.org › v2.0.0-alpha.27 › docs › Time-Zones
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
Since date-fns always returns a plain JS Date, which implicitly has the current system's time zone, helper functions are provided for handling common time zone related use cases. This function takes a Date instance in the system's local time ...
      » npm install date-fns-tz
    
Published   Sep 30, 2024
Version   3.2.0
Author   Marnus Weststrate
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'
//}
🌐
Date-fns
blog.date-fns.org › v40-with-time-zone-support
v4.0 is out with first-class time zones support!
As I mentioned, the time zones don’t add much overhead to the core library, and most of the functionality is handled by external and optional @date-fns/tz. The Bundlephobia says that the total package size grew from 17.2 kB to 17.5 kB. That is a very irrelevant number by itself as it includes every single function and locale, but it is also illustrative. I had to update almost all of the 250 functions, which only amounted to a 300 B increase, which I think is very good. As for TZDate itself, its minimal version, TZDateMini, is just 761 B, and the full version that adds strings formatting is 1 kB.
🌐
GitHub
github.com › date-fns › tz
GitHub - date-fns/tz: date-fns timezone utils · GitHub
Even though TZDate has a complete API, developers rarely use the formatter functions outside of debugging, so we recommend you pick the more lightweight TZDateMini for internal use. However, in environments you don't control, i.e., when you expose the date from a library, using TZDate will be a safer choice. Starting with v1.3.0, @date-fns/tz supports Format.JS polyfills that are required for Hermes JS Engine powering React Native runtime to work correctly.
Author   date-fns
🌐
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 - Let’s first understand what date-fns-tz allows us to do. With it, we are able to: Format dates in any time zone.
🌐
Mastering JS
masteringjs.io › tutorials › date-fns › tz
Working with Timezones using date-fns and date-fns-tz - Mastering JS
For example, the following is how ... "2023-08-02 02:00AM", 10 hours behind UTC formatInTimeZone(date, 'Pacific/Tahiti', 'yyyy-MM-dd HH:mma');...
🌐
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
Find elsewhere
🌐
UNPKG
unpkg.com › browse › date-fns-tz@1.0.7 › README.md
date-fns-tz
```javascript import { format, utcToZonedTime } from 'date-fns-tz' const date = new Date('2014-10-25T10:46:20Z') const nyTimeZone = 'America/New_York' const parisTimeZone = 'Europe/Paris' const nyDate = utcToZonedTime(date, nyTimeZone) const parisDate = utcToZonedTime(date, parisTimeZone) format(nyDate, 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: 'America/New_York' }) // 2014-10-25 06:46:20-04:00 format(nyDate, 'yyyy-MM-dd HH:mm:ss zzz', { timeZone: 'America/New_York' }) // 2014-10-25 06:46:20 EST format(parisDate, 'yyyy-MM-dd HH:mm:ss zzz', { timeZone: 'Europe/Paris' }) // 2014-10-25 10:46:20 GMT+2
🌐
Snyk
snyk.io › advisor › date-fns-tz › date-fns-tz code examples
Top 5 date-fns-tz Code Examples | Snyk
const {format} = require('date-fns-tz') const parseWithLuxon = require('.')(DateTime) const parseWithDateFns = require('./date-fns')(dateFns) // Europe/Berlin switched to DST at 31st of March at 2am. const withoutDST = '2019-03-31T01:59+01:00' const timeZone = 'Europe/Berlin' const rel = 'in 2 minutes' const dt = DateTime.fromISO(withoutDST).setZone(timeZone) const withDST1 = parseWithLuxon(rel, dt) console.log(withDST1.toFormat('HH:mm ZZZZ')) // 03:01 GMT+2 const withDST2 = parseWithDateFns(rel, new Date(withoutDST)) console.log(format(withDST2, 'HH:mm zz', {timeZone: 'Europe/Berlin'})) // 03:01 GMT+2 ·
🌐
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 date-fns always returns a plain JS Date, which implicitly has the current system's time zone, helper functions are provided for handling common time zone related use cases. This function takes a Date instance in the system's local time ...
Author   marnusw
🌐
GitHub
github.com › date-fns › date-fns › blob › main › docs › timeZones.md
date-fns/docs/timeZones.md at main · date-fns/date-fns
You can safely mix and match regular Date instances, as well as UTCDate or TZDate in different time zones and primitive values (timestamps and strings). date-fns will normalize the arguments, taking the first object argument (Date or a Date extension instance) as the reference and return the result in the reference type:
Author   date-fns
🌐
GitHub
github.com › prantlf › date-fns-timezone
GitHub - prantlf/date-fns-timezone: Parsing and formatting date strings using IANA time zones for date-fns.
] const timeZones = listTimeZones() // Set the date to "2018-09-01T16:01:36.386Z" const date = parseFromTimeZone('2018-09-01 18:01:36.386', { timeZone: 'Europe/Berlin' }) // Set the output to "1.9.2018 18:01:36.386 GMT+02:00 (CEST)" const date = new Date('2018-09-01Z16:01:36.386Z') const format = 'D.M.YYYY HH:mm:ss.SSS [GMT]Z (z)' const output = formatToTimeZone(date, format, { timeZone: 'Europe/Berlin' })
Starred by 136 users
Forked by 18 users
Languages   JavaScript 96.1% | TypeScript 3.9%
Top answer
1 of 2
10

You'll need two things to make this work correctly:

  • An IANA time zone identifier for the intended target time zone, such as 'America/Los_Angeles', rather than just an offset from UTC.

    • See "Time Zone != Offset" in the timezone tag wiki.
  • A library that supports providing input in a specific time zone.

    • Since you asked about date-fns, you should consider using the date-fns-tz add-on library.
    • Alternatively you could use Luxon for this.
    • In the past I might have recommended Moment with Moment-TimeZone, but you should review Moment's project status page before choosing this option.

Sticking with date-fns and date-fns-tz, the use case you gave is the very one described in the docs for the zonedTimeToUtc function, which I'll copy here:

Say a user is asked to input the date/time and time zone of an event. A date/time picker will typically return a Date instance with the chosen date, in the user's local time zone, and a select input might provide the actual IANA time zone name.

In order to work with this info effectively it is necessary to find the equivalent UTC time:

import { zonedTimeToUtc } from 'date-fns-tz'

const date = getDatePickerValue() // e.g. 2014-06-25 10:00:00 (picked in any time zone)
const timeZone = getTimeZoneValue() // e.g. America/Los_Angeles

const utcDate = zonedTimeToUtc(date, timeZone) // In June 10am in Los Angeles is 5pm UTC

postToServer(utcDate.toISOString(), timeZone) // post 2014-06-25T17:00:00.000Z, America/Los_Angeles

In your case, the only change is that at the very end instead of calling utcDate.toISOString() you'll call utcDate.getTime().

Note that you'll still want to divide by 1000 if you intend to pass timestamps in seconds rather than the milliseconds precision offered by the Date object.

2 of 2
-13

You can use 'moment' to convert timezone.

1.Create a moment with your date time, specifying that this is expressed as utc, with moment.utc()

2.convert it to your timezone with moment.tz()

For example

moment.utc(t, 'YYYY-MM-DD HH:mm:ss')
    .tz("America/Chicago")
    .format('l');
🌐
npm
npmjs.com › package › date-fns-timezone
date-fns-timezone - npm
Provides parsing and formatting date strings and time zone conversions supporting IANA time zones, following the design of functions in date-fns. List of canonical time zone names is provided by timezone-support.
      » npm install date-fns-timezone
    
Published   Oct 10, 2018
Version   0.1.4
Author   Ferdinand Prantl
🌐
Npm
npm.io › package › date-fns-tz
Date-fns-tz NPM | npm.io
Since date-fns always returns a plain JS Date, which implicitly has the current system's time zone, helper functions are provided for handling common time zone related use cases. This function takes a Date instance in the system's local time or an ISO8601 string, and an IANA time zone name ...