Artigo Build·Desenvolvimento·14 min de leitura

Fusos Horários em JavaScript: Guia Completo com Date, Intl e Temporal

Bugs de fuso horário em JavaScript são responsáveis por incontáveis erros em produção. Este guia cobre como Date funciona, Intl.DateTimeFormat, parsing seguro, lista de timezones do Brasil, horário de verão, a nova API Temporal e bibliotecas modernas.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

🕐

Converta timestamps Unix online

De timestamp para data legível em qualquer fuso horário — 100% no navegador.

Usar conversor de timestamp →

Bugs de fuso horário são responsáveis por uma boa parte dos problemas em produção em qualquer aplicação que lida com datas. Em JavaScript especificamente, a API Date tem comportamento contra-intuitivo que pega gente experiente — incluindo o famoso new Date("2026-04-18") que aparece como dia 17 no Brasil. Este guia cobre como Date funciona internamente, como usar Intl.DateTimeFormat para exibição correta, a lista completa de timezones do Brasil, parsing seguro, horário de verão e a nova API Temporal que está chegando.

Como o Date funciona internamente

O ponto mais importante para entender Date: ele não tem fuso horário. Internamente armazena apenas o número de milissegundos desde a Unix epoch (1 de janeiro de 1970 00:00:00 UTC). É um instante absoluto. O timezone é aplicado SOMENTE quando você formata para exibição ou lê componentes (getHours, getMinutes etc).

const now = new Date(); // Internamente: número absoluto, sem timezone now.getTime(); // ex: 1718812800000 // ISO 8601 (sempre UTC, marcado pelo Z final) now.toISOString(); // "2026-04-18T14:30:00.000Z" // String no fuso LOCAL do sistema (varia por máquina/usuário) now.toString(); // "Thu Apr 18 2026 11:30:00 GMT-0300 (BRT)" // Componentes UTC (independentes de timezone) now.getUTCHours(); // 14 now.getUTCMinutes(); // 30 // Componentes locais (dependem do timezone do sistema) now.getHours(); // 11 em BRT (UTC-3), 14 em UTC, 16 em CET now.getMinutes(); // 30

UTC vs horário local: a regra de ouro

Política recomendada

Backend e banco: sempre UTC. Cliente: sempre converta para o fuso do usuário no momento da exibição. Nunca armazene “horário local” sem timezone — vira fonte garantida de bugs.

Métodos UTC vs métodos locais do Date
CritérioUTC (independente de timezone)Local (depende do sistema)
AnosgetUTCFullYear()getFullYear()
MesesgetUTCMonth()getMonth()
Dia do mêsgetUTCDate()getDate()
Dia da semanagetUTCDay()getDay()
HorasgetUTCHours()getHours()
MinutosgetUTCMinutes()getMinutes()
ISO stringtoISOString() (sempre UTC)
String localtoString(), toLocaleString()

A armadilha do new Date(“YYYY-MM-DD”)

Esse é o bug clássico que pega quase todo dev JavaScript pelo menos uma vez:

// ❌ ARMADILHA — string ISO sem horário é UTC midnight const dataApi = new Date("2026-04-18"); dataApi.getDate(); // 17 (no Brasil!) dataApi.toString(); // "Fri Apr 17 2026 21:00:00 GMT-0300 (BRT)" // O JavaScript interpreta como 2026-04-18T00:00:00.000Z (UTC). // Brasil = UTC-3, então mostra 21h do dia 17. // ✅ Solução 1 — adicione horário explícito (assume timezone local) new Date("2026-04-18T00:00:00").getDate(); // 18 ✅ // ✅ Solução 2 — adicione offset explícito new Date("2026-04-18T00:00:00-03:00").getDate(); // 18 em BRT ✅ // ✅ Solução 3 — use construtor numérico new Date(2026, 3, 18).getDate(); // 18 (mês é 0-indexed!) ✅ // ✅ Solução 4 (preferida em 2026) — use Temporal Temporal.PlainDate.from("2026-04-18").day; // 18 ✅

Intl.DateTimeFormat: a forma certa de exibir

Para qualquer formatação visível ao usuário, prefira Intl.DateTimeFormat sobre métodos legados como toLocaleString. Suporta timezones IANA completos, internacionalização e padrões consistentes:

const d = new Date("2026-04-18T12:00:00Z"); // Formato brasileiro completo const fmt = new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo', dateStyle: 'full', timeStyle: 'short', }); fmt.format(d); // "sábado, 18 de abril de 2026 às 09:00" // Formato customizado new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }).format(d); // "18/04/2026, 09:00:00" // Múltiplos fusos no mesmo instante const fusos = ['America/Sao_Paulo', 'America/New_York', 'Europe/London', 'Asia/Tokyo']; fusos.forEach((tz) => { const t = new Intl.DateTimeFormat('pt-BR', { timeZone: tz, hour: '2-digit', minute: '2-digit', timeZoneName: 'short', }).format(d); console.log(`${tz.padEnd(20)}: ${t}`); });

Timezones do Brasil — lista IANA completa

O Brasil tem quatro timezones diferentes em uso. Sempre use o identificador IANA oficial (não invente “BRT”):

Timezones brasileiros (identificadores IANA)
CritérioOffsetEstados / regiões
America/Sao_PauloUTC-3 (BRT)Maioria do país: SP, RJ, MG, PR, SC, RS, GO, DF, BA, etc.
America/ManausUTC-4 (AMT)AM (maior parte), MT, MS, RO, RR
America/Rio_BrancoUTC-5 (ACT)AC e parte oeste do AM
America/NoronhaUTC-2Fernando de Noronha
America/BelemUTC-3PA (deprecated, use America/Sao_Paulo)
America/FortalezaUTC-3CE, MA, PI, RN, PB, PE, AL, SE (alias)

Brasil aboliu o horário de verão em 2019

Antes de 2019, alguns estados aplicavam DST (geralmente outubro a fevereiro). A partir de 2019, não há mais. Bibliotecas atualizadas (Intl, libs IANA) já refletem isso — mas se você tem código com cálculos manuais, revise.

Parsing seguro de datas vindas de API

A regra: APIs sempre devem trafegar datas em ISO 8601 com timezone explícito (UTC com Z final é o padrão). Parsing fica determinístico:

// ✅ ISO 8601 completo com Z (UTC) new Date("2026-04-18T14:30:00Z"); // ✅ ISO 8601 com offset new Date("2026-04-18T11:30:00-03:00"); // ⚠️ ISO sem timezone — assume horário local (frágil) new Date("2026-04-18T14:30:00"); // ❌ Formatos não-padrão (comportamento varia entre browsers!) new Date("18/04/2026"); // não use new Date("April 18, 2026"); // não use new Date("2026/04/18"); // não use // Para parsing de formatos brasileiros, use lib (date-fns): import { parse } from 'date-fns'; parse("18/04/2026", "dd/MM/yyyy", new Date());

Bibliotecas modernas: date-fns, Luxon, Day.js

Bibliotecas de manipulação de data em 2026
CritérioTamanhoTree-shakeableSuporte a timezone
date-fns~70kb (mas tree-shakeable)Sim, modularVia @date-fns/tz
Luxon~70kb minificadoNãoExcelente, nativo
Day.js~7kb com pluginsSimVia plugin (timezone)
Moment.js~290kbNãoVia moment-timezone (~70kb)
Date + Intl nativo0 (built-in)Bom, via Intl.DateTimeFormat
Temporal API0 (futuro)Excelente, first-class

date-fns (modular)

import { format, parseISO, addDays, differenceInDays } from 'date-fns'; import { ptBR } from 'date-fns/locale'; import { toZonedTime, formatInTimeZone } from 'date-fns-tz'; const d = parseISO("2026-04-18T14:30:00Z"); // Formatação localizada format(d, "EEEE, d 'de' MMMM 'de' y", { locale: ptBR }); // "sábado, 18 de abril de 2026" // Operações imutáveis const tomorrow = addDays(d, 1); differenceInDays(tomorrow, d); // 1 // Conversão para timezone específico formatInTimeZone(d, 'America/Sao_Paulo', "dd/MM/yyyy HH:mm"); // "18/04/2026 11:30"

Luxon (criado pelo autor do Moment)

import { DateTime } from 'luxon'; // Criação com timezone explícito const d = DateTime.fromISO("2026-04-18T14:30:00Z", { zone: 'utc', }); // Conversão para outro timezone const sp = d.setZone('America/Sao_Paulo'); sp.toFormat('dd/MM/yyyy HH:mm'); // "18/04/2026 11:30" // Operações fluentes const futuro = sp.plus({ days: 7, hours: 3 }); // Diferenças sp.diff(d, 'hours').hours; // -3 (BRT é 3h atrás de UTC)

A nova Temporal API (chegando)

Temporal é a substituta moderna do Date, em estágio 3 do TC39. Resolve quase todos os problemas históricos:

// Disponível em Node 22+ via flag, browsers via polyfill // Data sem horário (PlainDate) const data = Temporal.PlainDate.from("2026-04-18"); data.day; // 18 (sem confusão de timezone!) // Data + horário sem timezone (PlainDateTime) const dt = Temporal.PlainDateTime.from("2026-04-18T14:30:00"); // Instante absoluto (Instant) const inst = Temporal.Instant.from("2026-04-18T14:30:00Z"); // Instante + timezone (ZonedDateTime — mais útil) const sp = Temporal.ZonedDateTime.from( "2026-04-18T14:30:00+00:00[UTC]" ).withTimeZone('America/Sao_Paulo'); sp.toString(); // "2026-04-18T11:30:00-03:00[America/Sao_Paulo]" // Operações imutáveis e seguras const futuro = sp.add({ days: 7, hours: 3 }); sp.until(futuro, { largestUnit: 'days' }); // P7DT3H // Duração first-class const dur = Temporal.Duration.from({ hours: 5, minutes: 30 }); dur.total({ unit: 'minutes' }); // 330

Quando adotar Temporal

Para projetos novos com tolerância a polyfill, vale começar agora. Para projetos legados, espere suporte mais amplo (provavelmente em 2027). Usar polyfill @js-temporal/polyfill é seguro e é o caminho de migração.

Padrões corretos de armazenamento

Como armazenar datas em diferentes camadas
CritérioO que usar
Banco de dadosTIMESTAMP WITH TIME ZONE (Postgres) ou TIMESTAMP (MySQL) — sempre UTC
API JSONString ISO 8601 com Z final: '2026-04-18T14:30:00Z'
Em memória (JS)Date (UTC interno) ou Temporal.ZonedDateTime
URL / query stringISO 8601 URL-encoded ou Unix timestamp em segundos
Cache (Redis, etc.)Unix timestamp em ms ou ISO 8601 — nunca string local
LogsISO 8601 com Z (mais portável que timestamp puro)

Erros comuns que destroem produção

Lista negra de bugs de timezone

  • new Date("2026-04-18"): assume UTC, vira dia anterior em BRT. Use ISO completo.
  • Cálculos com offset fixo (-3h): quebra em transições de horário de verão em outros países.
  • Inventar abreviações: “BRT”, “EST” não são identificadores IANA. Use “America/Sao_Paulo”, “America/New_York”.
  • Armazenar “horário local” sem TZ: servidor mudou de máquina, datas viram outras.
  • setHours(0,0,0,0) para “início do dia”: em DST pode pular ou repetir horas.
  • Comparar Date com string: não faz o que você espera. Compare timestamps ou use isEqual da lib.
  • Usar Date.parse com formato não-ISO: comportamento varia entre browsers — não use.
  • Esquecer de testar em timezone diferente do dev: configure CI para rodar testes em UTC e em BRT.

Padrão recomendado para apps brasileiras

  1. Backend e banco em UTC. Configure timezone do servidor como UTC, mesmo em servidor brasileiro.
  2. API serializa em ISO 8601 com Z. Frontend recebe sempre UTC.
  3. Frontend usa Intl.DateTimeFormat com timeZone: 'America/Sao_Paulo' para exibir.
  4. Em forms, capture data + horário separados do timezone do usuário; converta para UTC antes de enviar.
  5. Logs sempre em UTC ISO, facilita correlação entre serviços em diferentes regiões.
  6. Testes em CI rodam com TZ=UTC e TZ=America/Sao_Paulopara pegar bugs de timezone.

Para datas sem horário (aniversário, evento)

Datas puras (aniversário, dia de cobrança, prazo) não devem virar Date com timezone — viram bug. Soluções:

  • Armazene como string YYYY-MM-DD (não Date).
  • Postgres tem tipo DATE puro — use, não timestamp.
  • Temporal.PlainDate resolve elegantemente quando disponível.

Checklist anti-bug de timezone

  • ✅ Servidor e banco configurados em UTC.
  • ✅ API trafega ISO 8601 com Z final.
  • ✅ Frontend usa Intl.DateTimeFormat para exibir.
  • ✅ Sempre identificadores IANA (America/Sao_Paulo).
  • ✅ Strings ISO sempre com horário OU offset explícito.
  • ✅ Cálculos via lib (date-fns/Luxon) ou Temporal — nunca offset fixo.
  • ✅ Datas puras armazenadas como YYYY-MM-DD ou DATE no banco.
  • ✅ Testes rodam em pelo menos 2 timezones diferentes.
  • ✅ Logs em UTC ISO para correlação multi-serviço.
  • ✅ Code review especificamente atento a Date.parse e cálculos manuais.

Perguntas frequentes

O Date do JavaScript tem fuso horário?+

Não. Internamente, Date armazena apenas o número de milissegundos desde 1 de janeiro de 1970 UTC (Unix epoch). É um instante absoluto no tempo, sem timezone. O fuso horário é aplicado SOMENTE quando você formata para exibição (toString, toLocaleString) ou quando lê os componentes (getHours, getMinutes). Por isso o mesmo Date renderiza diferente em diferentes regiões do mundo.

Por que new Date("2026-04-18") aparece como dia 17 no Brasil?+

Porque strings ISO sem horário são interpretadas como UTC midnight (00:00:00 UTC). O Brasil é UTC-3, então 00:00 UTC é 21:00 do dia anterior em horário de Brasília. Para evitar: especifique horário ("2026-04-18T00:00:00") para usar timezone local, ou inclua offset explícito ("2026-04-18T00:00:00-03:00"). Esse é o bug clássico de timezone em JavaScript.

Qual é o melhor jeito de exibir uma data formatada por região?+

Intl.DateTimeFormat. Sempre. É a API nativa para internacionalização e suporta todos os timezones IANA. Para datas formatadas: new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo', dateStyle: 'full', timeStyle: 'short' }).format(date). Funciona em todos os browsers modernos e Node.js, sem dependência externa.

Devo usar Moment.js em 2026?+

Não para projetos novos. Moment.js está em modo de manutenção desde 2020 — os próprios mantenedores recomendam migrar. Alternativas modernas: date-fns (modular, tree-shakeable), Luxon (do mesmo autor do Moment, mais moderno), Day.js (drop-in replacement quase compatível). Para uso simples sem timezone complexo, Date + Intl nativos são suficientes.

O que é a Temporal API?+

Temporal é a substituta moderna do Date no JavaScript, atualmente em estágio 3 do TC39. Resolve quase todos os problemas históricos: timezone first-class, datas sem horário, durações imutáveis, parsing seguro. Em 2026 está disponível em browsers modernos via flag e em Node 22+. Para projetos novos que podem esperar suporte amplo, vale considerar polyfill.

Quantos timezones tem o Brasil?+

Quatro timezones diferentes em uso: BRT (UTC-3, maioria do país e horário oficial), AMT (UTC-4, AM, MT, MS, RO, RR), ACT (UTC-5, AC e parte do AM), Fernando de Noronha (UTC-2). O Brasil aboliu o horário de verão em 2019, então não há mudança automática anual. Use sempre identificadores IANA como 'America/Sao_Paulo' (não inventar abreviações).

Como armazenar datas no banco para evitar bugs de timezone?+

Sempre em UTC com tipo timestamp/timestamptz. Nunca armazene 'horário local' como string sem timezone. Em PostgreSQL: TIMESTAMP WITH TIME ZONE (timestamptz). Em MySQL: TIMESTAMP (já assume UTC). No JavaScript, sempre serialize como ISO 8601 com Z final (toISOString()). A conversão para timezone do usuário é responsabilidade da camada de apresentação.

Como lidar com horário de verão em outros países?+

Use sempre identificadores IANA como 'America/New_York' (não 'EST' ou 'EDT'). A biblioteca interna do JavaScript (e do banco) sabe quando aplicar +1h. Evite cálculos manuais com offsets fixos — eles quebram em transições. Se você processa eventos futuros, armazene timezone do usuário separadamente; cálculos baseados em ISO + IANA tz dão a hora correta mesmo se as regras mudarem.

#javascript#fusos horários#timezone#date#intl#temporal#date-fns#luxon#iana#utc

Continue lendo