12k
All articles

Lidando com Fusos Horários em JavaScript Sem Perder a Cabeça

Lide com fusos horários em JavaScript usando instantes UTC, IDs IANA, Intl.DateTimeFormat, Temporal e regras seguras para DST ao salvar e exibir datas.

OpenReplay Team
OpenReplay Team
Lidando com Fusos Horários em JavaScript Sem Perder a Cabeça

Armazene e transmita cada timestamp como um instante UTC em ISO 8601 (por exemplo, 2026-05-22T08:00:00Z), mantenha o identificador de fuso horário IANA (por exemplo, America/New_York) em um campo separado e converta para o horário local apenas no momento da exibição — nunca armazene um horário de relógio sem o seu fuso. Essa única regra previne a maioria dos bugs de fuso horário em JavaScript, e ela se aplica independentemente de você usar Date, Intl, uma biblioteca ou a nova API Temporal.

Este guia aborda por que o Date nativo torna os fusos horários tão problemáticos, as regras duradouras que resolvem o problema independentemente das ferramentas utilizadas, como formatar datas corretamente hoje com Intl.DateTimeFormat, o que o Temporal muda agora que foi incluído no ES2026, qual biblioteca utilizar em produção a partir de junho de 2026 e os casos extremos de horário de verão que causam os bugs mais difíceis de reproduzir.

Principais Conclusões

  • Armazene e transmita instantes em UTC (ISO 8601 ou epoch), mantenha o identificador de fuso horário IANA em um campo separado e converta para horário local apenas na exibição.
  • O Date do JavaScript não possui suporte a fusos horários nomeados — ele só consegue representar um momento em UTC ou no fuso da máquina host, o que é a causa raiz do problema “data correta na minha máquina, data errada para o usuário.”
  • Um evento futuro deve ser armazenado com seu fuso horário IANA, e não como um instante UTC fixo, para que ainda seja resolvido para o horário de relógio correto caso as regras de horário de verão daquela região mudem antes que a data chegue.
  • A partir de junho de 2026, Temporal é uma proposta Stage 4 no ECMAScript 2026, disponível nativamente no Firefox 139+, Chromium 144+ e Node.js 26+, mas não no Safari — portanto, o código em produção ainda geralmente precisa de @js-temporal/polyfill ou temporal-polyfill.
  • Se você ainda não puder utilizar Temporal, use Luxon 3.7.2 ou date-fns 4.4.0 com @date-fns/tz — e observe que o pacote mais antigo date-fns-tz é destinado ao date-fns v3, não ao v4.

Por Que o Date do JavaScript Faz Você Perder a Cabeça

O objeto Date possui três falhas estruturais, sendo a terceira o grande problema com fusos horários. Primeiro, ele é mutável: métodos como setMonth e setFullYear modificam o objeto original diretamente, de modo que passar um Date para uma função pode alterá-lo silenciosamente para todos os outros chamadores. Segundo, sua numeração é inconsistente — os meses são indexados a partir de zero (janeiro é 0, dezembro é 11), enquanto os dias do mês são indexados a partir de um — o que gera bugs de deslocamento de um mês que sobrevivem à revisão de código.

Terceiro, e mais grave: o Date não possui suporte real a fusos horários. Ele só consegue representar um momento em UTC ou no fuso local da máquina host, e nada além disso — não há como construir ou trabalhar com um Date “em America/New_York” da forma que se esperaria. O texto oficial da proposta TC39 afirma isso claramente: o tradicional objeto Date do ECMAScript apresenta uma série de desafios, incluindo falta de imutabilidade, falta de suporte a fusos horários, falta de suporte a casos de uso que exigem apenas datas ou apenas horários, e uma API confusa e pouco ergonômica.

A renderização baseada no fuso da máquina host é o motivo pelo qual o mesmo código exibe a data correta para um desenvolvedor em Berlim e a data errada para um usuário em Los Angeles. Considere uma reprodução de 8 linhas que você pode executar com o Node:

// repro.js — execute com: TZ=America/Los_Angeles node repro.js
//          e novamente: TZ=Europe/Berlin node repro.js
const instant = new Date("2026-03-15T23:30:00Z"); // um momento UTC fixo
console.log(instant.toLocaleDateString());
// TZ=America/Los_Angeles → "3/15/2026"  (16:30 local, ainda é dia 15)
// TZ=Europe/Berlin       → "3/16/2026"  (00:30 local, já é dia 16)

Um único instante, duas datas de calendário diferentes, dependendo apenas do fuso da máquina host. O bug é invisível para quem o escreveu, pois sua máquina está em um único fuso. Esse bug de data baseado no fuso host é o clássico defeito do tipo “funciona na minha máquina.”

As Regras Duradouras Que Resolvem os Fusos Horários (Independentemente de Qualquer Biblioteca)

As regras abaixo previnem bugs de fuso horário independentemente de qual API ou biblioteca você utilize. Elas são a solução real; as ferramentas que se seguem são apenas formas diferentes de aplicá-las.

  1. Armazene e transmita instantes em UTC. Persista timestamps como ISO 8601 com sufixo Z (2026-05-22T08:00:00Z) ou como valor epoch. UTC é inequívoco e nunca sofre deslocamentos.
  2. Mantenha o identificador de fuso horário IANA em um campo separado. Um fuso como Europe/London carrega as regras de horário de verão que um simples offset não consegue representar. Armazene o identificador, não um offset bruto como +01:00.
  3. Converta para horário local apenas na borda — no momento da exibição. Mantenha tudo em UTC durante o armazenamento, o transporte e a lógica de negócios; localize apenas na camada de visualização.
  4. Distinga um instante absoluto de um horário de relógio em um fuso específico. Uma entrada de log ou um valor de “criado em” é um instante. Uma reunião no calendário de alguém é um horário de relógio vinculado a um fuso. São tipos de dados diferentes e devem ser modelados de forma diferente.
  5. Armazene eventos futuros com fuso horário, não como um instante UTC fixo. Esta é a regra que quase ninguém menciona. Se um usuário agenda uma reunião para as 9:00 da manhã em America/New_York com dois anos de antecedência e aquela região posteriormente altera suas regras de horário de verão, um timestamp UTC congelado hoje resolverá para o horário de relógio errado. Armazenar o fuso permite que o instante seja recalculado quando a data chegar.

Essa última regra tem base em uma fonte primária. A serialização padrão que o Temporal utiliza, a RFC 9557 (o Formato Estendido de Data/Hora da Internet, publicada em abril de 2024), existe precisamente porque, como a Igalia observa, o Temporal precisa de uma forma padronizada de serializar timestamps com informações de fuso horário e calendário, mas as convenções amplamente utilizadas — como acrescentar nomes de fuso horário IANA aos timestamps — nunca haviam sido formalmente padronizadas. O MDN apresenta a versão operacional da regra para offset versus fusos nomeados: evite usar identificadores de offset quando houver um fuso horário nomeado disponível. Mesmo que uma região sempre tenha utilizado um único offset, é preferível usar o identificador nomeado para se proteger contra futuras mudanças políticas no offset.

Exibição Localizada Hoje Com Intl.DateTimeFormat

Para uma exibição localizada correta agora mesmo, use Intl.DateTimeFormat com a opção timeZone explícita. É o único recurso nativo que lida corretamente com fusos nomeados, está disponível em todos os navegadores modernos e no Node, e funciona em conjunto tanto com Date quanto com Temporal.

const instant = new Date("2026-03-15T23:30:00Z");

new Intl.DateTimeFormat("pt-BR", {
  timeZone: "America/New_York",
  dateStyle: "full",
  timeStyle: "short",
}).format(instant);
// "domingo, 15 de março de 2026 às 19:30"

Passar timeZone explicitamente é o que torna isso seguro: você não está mais à mercê do fuso da máquina host. Renderizar o mesmo instante para um usuário diferente significa alterar apenas uma string. Esta é a regra de “converter na borda” em código — mantenha o instante UTC em todo o sistema e deixe o Intl cuidar da localização na camada de visualização.

Temporal: A Solução para Fusos Horários em JavaScript Integrada à Linguagem

Temporal é o substituto há muito prometido para o Date, e a partir de 2026 ele é uma realidade. Após 9 anos de trabalho, na reunião do TC39 de março de 2026, o Temporal atingiu oficialmente o Stage 4, tornando-se parte do ECMAScript 2026. O repositório da proposta confirma o status diretamente: esta proposta está atualmente no Stage 4. Ela será incorporada aos padrões ECMA-262 e ECMA-402 e este repositório será arquivado.

O Temporal substitui o Date por um namespace de tipos imutáveis e com propósitos específicos. Os três que você mais utilizará:

  • Temporal.Instant — um momento exato no tempo (um timestamp em nanossegundos), sem calendário ou fuso. Use-o para os instantes UTC da regra 1.
  • Temporal.ZonedDateTime — um instante mais um fuso horário IANA mais um calendário. O MDN o descreve como uma ponte entre um tempo exato e um horário de relógio: ele representa simultaneamente um instante na história e um horário local de relógio. É a única classe do Temporal que é consciente de fuso horário. Use-o para eventos futuros com fuso (regra 5).
  • Temporal.PlainDate / Temporal.PlainTime — uma data de calendário ou um horário de relógio sem nenhum fuso, para coisas como aniversários e horários de funcionamento de estabelecimentos.

A aritmética é imutável — cada operação retorna um novo valor — e a conversão de um momento entre fusos é explícita:

const callAmsterdam = Temporal.ZonedDateTime.from(
  "2026-04-24T15:00:00[Europe/Amsterdam]"
);
const callNewYork = callAmsterdam.withTimeZone("America/New_York");
callNewYork.toString();
// "2026-04-24T09:00:00-04:00[America/New_York]"

O Temporal também elimina uma armadilha perigosa do Date: os operadores de comparação em objetos Temporal lançam um TypeError por design, pois na ausência de valueOf(), expressões com operadores aritméticos como plainDate1 > plainDate2 recairiam para o equivalente a plainDate1.toString() > plainDate2.toString(). Use Temporal.compare() ou .equals()Temporal.compare() ordena dois valores com fuso pelo instante subjacente, tratando 9:30 da manhã em Nova York e 14:30 em Londres como iguais, enquanto .equals() os reporta como diferentes porque também compara o fuso horário e o calendário. Para a lista completa de tipos, consulte a referência Temporal no MDN.

Suporte do Temporal em navegadores e runtimes (a partir de junho de 2026)

O Temporal está sendo lançado, mas ainda não em todos os ambientes. O suporte nativo chegou ao Firefox 139, que se tornou o primeiro navegador a disponibilizar o Temporal por padrão em maio de 2025, seguido pelo Chrome 144 em janeiro de 2026. O Edge utiliza o mesmo motor Chromium, e o Node.js também o incluiu: o Node.js 26, lançado em 5 de maio de 2026 com V8 14.6 e Undici 8, habilitou o Temporal sem nenhuma flag ou configuração experimental. Pela primeira vez na história do JavaScript, os desenvolvedores têm uma API de data/hora de primeira classe integrada diretamente ao runtime.

A lacuna é o Safari, que ainda não o disponibilizou — e é exatamente por isso que o MDN marca o Temporal como ainda não sendo Baseline. Para código em produção com suporte cross-browser, você ainda precisa de um polyfill. Existem dois: @js-temporal/polyfill, mantido pelos responsáveis pela proposta, e temporal-polyfill, uma alternativa menor e mais rápida da equipe do FullCalendar que oferece compatibilidade cross-browser para os navegadores restantes. O status oficial deles no repositório da proposta é alpha/beta, e não uma versão estável 1.0, portanto fixe uma versão e teste antes de colocar em produção. Verifique o tamanho do bundle comprimido no Bundlephobia antes de incluí-lo no orçamento; os valores publicados variam bastante.

Qual Biblioteca Usar em Produção Agora

Se você não pode depender do Temporal nativo em todos os seus ambientes — e a maioria dos aplicativos em produção não pode, até que o Safari o disponibilize e você tenha descartado o polyfill — recorra a uma das opções abaixo. O veredicto, a partir de junho de 2026:

FerramentaConsciente de fuso?Imutável?Nativo hoje?Use quando
Date + Intl.DateTimeFormatApenas na exibiçãoNão (Date é mutável)SimNecessidades mínimas; formatar um instante existente
Luxon 3.7.2Sim (IANA)SimSimNovo código que deseja uma API ergonômica e imutável próxima ao Temporal
date-fns 4.4.0 + @date-fns/tzSim (IANA)SimSimBases de código com tree-shaking e importação por função
Day.js + plugins utc/timezoneSim (IANA)SimSimMenor footprint; migração do Moment.js
Temporal (nativo ou polyfill)Sim (nativo)SimParcialAmbientes evergreen controlados/Node, ou com polyfill

A armadilha de precisão mais importante está na coluna do date-fns. O suporte a fusos horários mudou entre as versões principais: a partir da v4, o date-fns possui suporte nativo a fusos horários. Ele é fornecido pelos pacotes @date-fns/tz e @date-fns/utc. A abordagem da v4 utiliza a classe TZDate e o helper tz() do @date-fns/tz (v1.5.0). O pacote mais antigo date-fns-tz (v3.2.0) é destinado ao date-fns v3 e é explícito sobre isso — sua própria documentação indica que deve ser usado caso você precise de suporte a fusos horários com versões anteriores ao date-fns v4. Não os misture.

Os Casos Extremos de Horário de Verão Que Realmente Causam Problemas

O horário de verão produz dois modos de falha, e ambos são pouco testados na maioria das bases de código. No outono (quando os relógios são atrasados), uma hora local ocorre duas vezes, tornando um horário de relógio como 01:05 ambíguo. Na primavera (quando os relógios são adiantados), uma hora local nunca existe, tornando um horário como 02:05 inválido.

O Temporal resolve ambos de forma determinística. Ele é resolvido usando o comportamento de desambiguação “compatible”: o instante posterior dos dois possíveis será usado para transições de tempo pulado, e o instante anterior dos dois possíveis será usado para transições de tempo repetido. Os resultados:

// Atraso de horário: 01:05 ocorre duas vezes em Nova York em 2024-11-03
Temporal.ZonedDateTime.from("2024-11-03T01:05:00[America/New_York]").toString();
// "2024-11-03T01:05:00-04:00[America/New_York]"  (padrão: o instante anterior)
Temporal.ZonedDateTime.from("2024-11-03T01:05:00[America/New_York]",
  { disambiguation: "later" }).toString();
// "2024-11-03T01:05:00-05:00[America/New_York]"  (a segunda ocorrência)

// Avanço de horário: 02:05 não existe em Nova York em 2024-03-10
Temporal.ZonedDateTime.from("2024-03-10T02:05:00[America/New_York]").toString();
// "2024-03-10T03:05:00-04:00[America/New_York]"  (padrão: avança uma hora)

Para a hora pulada, você também pode passar disambiguation: "reject" para lançar uma exceção em vez de resolver silenciosamente — útil quando uma reserva cai em um horário inexistente e você prefere alertar o usuário a tentar adivinhar. Com o Date, nada disso é tratado automaticamente, e o bug só aparece para usuários em fusos que observam o horário de verão, nos dois dias do ano em que a transição ocorre.

Defeitos de fuso horário são difíceis de corrigir precisamente porque não se reproduzem no fuso do desenvolvedor. Uma contagem regressiva exibe valor negativo, um card de evento mostra o dia errado, uma reserva cai no lado errado de uma transição de horário de verão — mas apenas para o usuário, nunca na máquina que escreveu o código. A reprodução de sessão frequentemente é a única forma prática de fechar essa lacuna: reproduzir uma sessão capturada no ambiente do usuário permite que um desenvolvedor em Europe/Berlin veja exatamente a data errada que um usuário em America/Los_Angeles visualizou, em vez de tentar imaginá-la.

Por Onde Continuar

A solução para bugs de fuso horário não é uma biblioteca — é a disciplina de armazenar instantes em UTC, manter o fuso IANA junto, converter apenas na exibição e modelar eventos futuros com fuso horário. Aplique essas regras primeiro, depois escolha a ferramenta: Temporal nativo onde seus ambientes o suportam, um polyfill onde não suportam, e Luxon ou date-fns v4 com @date-fns/tz para tudo o que estiver entre esses casos. Comece auditando um lugar na sua base de código onde um horário de relógio é armazenado sem seu fuso — esse campo é quase certamente onde o próximo bug de deslocamento de um dia está esperando.

Perguntas Frequentes

Por que minha data aparece com um dia de diferença para alguns usuários, mas não para mim?

Um Date do JavaScript armazena apenas um instante UTC, e métodos como toLocaleDateString o renderizam no fuso da máquina host. Um instante fixo como 2026-03-15T23:30:00Z é renderizado como 15 de março em America/Los_Angeles (16:30 local), mas como 16 de março em Europe/Berlin (00:30 local). O código está correto; a data do calendário difere porque o fuso de renderização difere. É por isso que o bug nunca se reproduz no fuso horário do desenvolvedor.

Devo armazenar o horário de uma reunião futura como um timestamp UTC?

Não. Armazene um evento futuro como um horário de relógio vinculado ao seu fuso IANA, como 2026-05-22T09:00:00 com America/New_York mantido junto, e não como um instante UTC fixo. Se a região alterar suas regras de horário de verão entre agora e a data do evento, um timestamp UTC calculado hoje resolverá para o horário de relógio errado, enquanto o valor com fuso pode ser recalculado. Instantes UTC são corretos para logs e eventos passados, não para compromissos futuros.

Qual é a diferença entre date-fns-tz e @date-fns/tz?

Eles são destinados a versões principais diferentes e não são intercambiáveis. O pacote mais antigo date-fns-tz (v3.2.0) fornece suporte a fusos horários apenas para o date-fns v3. A partir do date-fns v4, o suporte a fusos horários foi movido para o pacote separado @date-fns/tz (v1.5.0), que fornece a classe TZDate e o helper tz. Se você está no date-fns 4.x, use @date-fns/tz; misturar os dois com a versão principal errada é uma fonte comum de conversões incorretas.

Posso usar a API Temporal em produção em 2026?

Parcialmente. A partir de junho de 2026, o Temporal é uma proposta Stage 4 no ECMAScript 2026 e está disponível nativamente no Firefox 139+, Chromium 144+ (Chrome e Edge) e Node.js 26+. O Safari ainda não o disponibilizou, razão pela qual o MDN marca o Temporal como ainda não sendo Baseline. Para ambientes evergreen controlados ou de servidor, você pode usá-lo nativamente; para amplo suporte em navegadores, você ainda precisa de @js-temporal/polyfill ou temporal-polyfill, ambos com status alpha ou beta, portanto fixe uma versão e teste primeiro.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.