Por que Date não tem propriedade de fuso
Um Date do JavaScript é sempre uma contagem de milissegundos UTC. Não há fuso armazenado dentro do objeto. Quando você chama .toString() ou .toLocaleString() sem opções, o JavaScript usa o fuso local do runtime a partir do sistema operacional. O mesmo código produz saídas diferentes em um servidor em Nova York e em um notebook em Tóquio, mesmo que o timestamp subjacente seja idêntico.
Formatar com Intl.DateTimeFormat
Intl.DateTimeFormat é a API integrada para formatação ciente de localidade e fuso. Ela aceita identificadores de fuso IANA, trata as transições de horário de verão automaticamente e está disponível em navegadores modernos e no Node.js. O segredo é passar a opção timeZone explicitamente em vez de confiar no padrão do runtime.
- new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', dateStyle: 'full', timeStyle: 'long' }).format(date)
- date.toLocaleString('en-GB', { timeZone: 'Europe/London', hour12: false })
- date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' })
- new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', hour12: false }).format(date)
Extrair partes individuais com formatToParts
Use formatToParts() para obter os componentes individuais da data como objetos {type, value} e montar strings de data personalizadas. Isso é melhor do que dividir uma string de data localizada porque pontuação, ordem e escritas diferem por localidade.
- const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Chicago', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date)
- parts.find(p => p.type === 'year').value → '2023'
- parts.find(p => p.type === 'month').value → '11'
- Object.fromEntries(parts.map(p => [p.type, p.value])) → { year, month, day, hour, minute, second }
Converter hora de relógio em UTC (o sentido difícil)
Ir de uma hora de relógio em um fuso dado para UTC é mais difícil com a API Date clássica. Formatar um instante em America/New_York é fácil; construir o instante representado por 2026-03-08 02:30 em America/New_York não é, pois essa hora local pode ser pulada ou repetida durante as transições de horário de verão.
- O Temporal chegou ao Estágio 4 em 2026, mas o suporte nativo dos navegadores ainda não é universal
- Para uso em produção em todos os navegadores hoje, o polyfill do Temporal ou date-fns-tz toDate() ainda são escolhas práticas
- Evite aritmética manual de offset UTC — as transições de horário de verão ocorrem em horas locais diferentes em anos diferentes
- O zonedToEpochMs() deste site usa correção de offset de uma iteração — veja src/timeUtils.ts
Escolher o identificador de fuso correto
Use nomes de fuso IANA como America/New_York, Europe/London, Asia/Tokyo ou UTC. Evite offsets fixos como UTC-5 para a hora voltada ao usuário porque os offsets mudam com o horário de verão. America/New_York pode ser UTC-5 em janeiro e UTC-4 em julho; o nome IANA permite à plataforma aplicar a regra histórica correta para a data selecionada.
- Bom: America/Los_Angeles — inclui regras de horário de verão históricas e futuras
- Bom: Europe/Berlin — trata o horário de verão da Europa Central automaticamente
- Bom: Asia/Shanghai — exibição estável UTC+8 sem horário de verão
- Use UTC para logs, armazenamento de API, timestamps de banco e comparação entre regiões
- Use o fuso IANA do usuário para a exibição final na interface do produto
FAQ sobre formatação de fusos
Two daylight-saving transitions a year break the assumption that "wall-clock time + timezone = a unique moment". Both are encoded in the IANA timezone database (still often called the Olson database) — the authoritative source of timezone rules used by every modern OS, browser, and language runtime.
- DST gap: 02:00 to 02:59 local time does not exist on the spring-forward day
- DST overlap: 01:00 to 01:59 local time occurs twice on the fall-back day
- Ambiguous local time: in the overlap window you must pick "earlier" or "later" explicitly
- Python uses a fold flag; JS Temporal uses a disambiguation option; java.time defaults to the earlier offset
- Olson database / IANA tzdata / zoneinfo are three names for the same dataset, updated several times a year
- Each IANA name (America/Los_Angeles, Europe/Berlin) maps to a list of (transition, offset, abbreviation) tuples
- Stale tzdata on a device produces silently-wrong local times after any DST or zone change
- Server best practice: do all date arithmetic in UTC, convert to local only at display
FAQ
- Can JavaScript format a date in another timezone without a library?
- Yes. Use Intl.DateTimeFormat or toLocaleString with a timeZone option, such as America/New_York or Asia/Tokyo.
- Does Intl.DateTimeFormat handle daylight saving time?
- Yes. When you use an IANA timezone identifier, the runtime applies the correct offset for that timezone and date.
- Should I store timezone offsets or timezone names?
- Store UTC for event instants. If you need the user's local context, store an IANA timezone name such as America/New_York, not only a numeric offset.
- How do I get the user's timezone in JavaScript?
- Use Intl.DateTimeFormat().resolvedOptions().timeZone, which returns an IANA name like America/New_York. Store that name, not a numeric offset, when you need to reconstruct the user's local time later.
- What is the DST gap (spring-forward)?
- The DST gap is the wall-clock hour that is skipped when daylight saving time begins. In most US zones, when the clock would tick from 02:00 to 02:59 it instead jumps to 03:00. Local times in that window do not exist on the transition day. Unix timestamps themselves never skip; only the wall-clock interpretation does.
- What is the DST overlap (fall-back)?
- The DST overlap is the wall-clock hour that occurs twice when daylight saving time ends. In most US zones, the clock ticks from 01:59 back to 01:00, so every minute from 01:00 to 01:59 happens twice. A wall-clock time in that window maps to two Unix timestamps; converters and schedulers must specify "earlier" or "later".