An Angular invoice's due date shows one day early for some users; how do DatePipe's input parsing and time-zone argument explain it, and how do you fix it?
answer
- an instant versus a calendar date
- user's local zone by default
- DATE_PIPE_DEFAULT_OPTIONS
- offsets, not zone names
basics
~20 sAn instant such as UTC midnight is formatted in each user's local zone by default, so western users see the previous day. Send a date-only string, or pass a zone argument or DATE_PIPE_DEFAULT_OPTIONS timezone such as 'UTC'.
solid answer
~30 s`DatePipe` formats in the zone given as its second argument, else the `timezone` in `DATE_PIPE_DEFAULT_OPTIONS`, else the user's local zone. If the API sends `'2026-03-01T00:00:00Z'`, that UTC midnight is still Feb 28 in the Americas. For a calendar date with no time meaning, send `'2026-03-01'`: `DatePipe` builds a date-only string as local midnight of that day, so it never shifts. For instants that must read in one zone, use `date: 'mediumDate' : 'UTC'` or provide the options token once. Beware that the zone argument expects an offset like `'+0100'` or `'UTC'`; a name like `'Europe/Berlin'` is silently ignored.
code
ts · 8 linesimport { ApplicationConfig } from '@angular/core';
import { DATE_PIPE_DEFAULT_OPTIONS } from '@angular/common';
export const appConfig: ApplicationConfig = {
providers: [
{ provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { dateFormat: 'mediumDate', timezone: 'UTC' } },
],
};go deeper
Recall that DatePipe accepts a format and an optional time-zone argument, and that without one it uses the user's local zone.
Explain the lookup order for the zone and how DatePipe treats date-only strings versus ISO strings with a time and Z.
Diagnose off-by-one dates by checking the payload shape and zone configuration, fix it in the contract, and catch Y-versus-y pattern bugs.
Define how the system models calendar dates versus instants end to end, so no screen has to guess which one it is displaying.
## The symptom An invoice shows a due date of **Feb 28** for some customers and **Mar 1** for others, although the backend stores one value. The template is ordinary: ```html Due {{ invoice.dueDate | date: 'mediumDate' }} ``` The cause is almost always the combination of **how `DatePipe` parses its input** and **which time zone it formats in**. ## Step 1: how `DatePipe` turns the value into a `Date` `DatePipe` accepts a `Date`, a number of milliseconds, or a string. For strings it distinguishes two cases: - a **date-only** ISO string such as `'2026-03-01'` is built as **local midnight** of that calendar date, deliberately avoiding the browser's habit of parsing date-only strings as UTC; - a **date-time** ISO string such as `'2026-03-01T00:00:00Z'` is an exact instant; the trailing `Z` means UTC midnight. If the API sends the due date as UTC midnight, the instant is correct, but it is still Feb 28 evening in the Americas. ## Step 2: which time zone it formats in The zone is chosen in this order: 1. the pipe's **second argument**, as in `date: 'mediumDate' : 'UTC'`; 2. the `timezone` field of the **`DATE_PIPE_DEFAULT_OPTIONS`** token, if provided; 3. otherwise the **end user's local system time zone**. With no argument and no token, each user sees the instant converted to their own zone, which moves UTC midnight to the previous day west of Greenwich. ## Step 3: fix it at the right layer | Situation | Fix | |---|---| | The value is a calendar date with no time meaning (due date, birth date) | Send a date-only string such as `'2026-03-01'`, so the pipe builds local midnight of that date | | The value is an instant, but invoices should read in one fixed zone | Pass the zone: `date: 'mediumDate' : 'UTC'`, or provide `DATE_PIPE_DEFAULT_OPTIONS` once | | The value is an instant and users should see their own local time | Leave the default, and label the time zone on screen | Providing a default for the whole application: ```ts providers: [{ provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { timezone: 'UTC' } }] ``` The older `DATE_PIPE_DEFAULT_TIMEZONE` token still exists but is deprecated in favour of `DATE_PIPE_DEFAULT_OPTIONS`. ## The trap inside the fix: what the zone argument accepts The time-zone argument is documented as an **offset** such as `'+0430'`, and `'UTC'` also works. It is not a full IANA-name API: Angular converts the string by asking `Date.parse` for an offset, and if that yields nothing usable it **silently falls back** to the date's own local offset. So a value like `'Europe/Berlin'` produces no error and no conversion. A fixed offset such as `'+0100'` also ignores daylight saving time. When you need real named-zone rules, format with the platform's `Intl` API in the component instead. ## A second classic: `YYYY` versus `yyyy` In `DatePipe` patterns, `y` is the **calendar year** and `Y` is the **ISO week-numbering year**, which differs from the calendar year for a few days around January 1. A pattern like `'dd/MM/YYYY'` would print the wrong year for, say, December 30. Current Angular guards this in development builds: a pattern containing `Y` without a week field `w` throws error `NG02300` ("Suspicious use of week-based year") wrapped as an invalid pipe argument, while the bare pattern `'YYYY'` only logs it. The check does not run in production builds, so the fix is to use `yyyy` and let the development error do its job. ## How to diagnose quickly 1. Log the raw value the template receives: is it date-only or a date-time with `Z` or an offset? 2. Check whether a zone argument or `DATE_PIPE_DEFAULT_OPTIONS` is in play, and whether its value is an offset `DatePipe` actually understands. 3. Reproduce by switching the operating system's time zone, or by testing the formatting with an explicit zone argument. 4. Scan date patterns for uppercase `Y`.
- Why does the pattern 'dd/MM/YYYY' throw in development but print a wrong year in production around New Year?`Y` is the ISO week-numbering year. In development builds, a pattern with `Y` but no week field `w` throws `NG02300` ("Suspicious use of week-based year"); the check is skipped in production. Use `yyyy` for the calendar year.
- Why does date:'short':'Europe/Berlin' show the user's local time instead of Berlin time?The zone argument is converted to an offset through `Date.parse`; a name it cannot turn into an offset falls back silently to the date's own local offset. Use an offset such as `'+0100'`, accepting that it ignores daylight saving, or format with the platform's Intl API in the component.
saying these in an interview costs you the question
- Believes DatePipe always formats dates in UTC
- Thinks an IANA zone name is fully supported by DatePipe's zone argument
- Treats a due date as a UTC-midnight timestamp without consequences
- Uses YYYY in patterns believing it means calendar year
- Thinks a fixed offset like +0100 follows daylight saving changes