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.
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(); // 30UTC 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.
| Critério | UTC (independente de timezone) | Local (depende do sistema) |
|---|---|---|
| Anos | getUTCFullYear() | getFullYear() |
| Meses | getUTCMonth() | getMonth() |
| Dia do mês | getUTCDate() | getDate() |
| Dia da semana | getUTCDay() | getDay() |
| Horas | getUTCHours() | getHours() |
| Minutos | getUTCMinutes() | getMinutes() |
| ISO string | toISOString() (sempre UTC) | — |
| String local | — | toString(), 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”):
| Critério | Offset | Estados / regiões |
|---|---|---|
| America/Sao_Paulo | UTC-3 (BRT) | Maioria do país: SP, RJ, MG, PR, SC, RS, GO, DF, BA, etc. |
| America/Manaus | UTC-4 (AMT) | AM (maior parte), MT, MS, RO, RR |
| America/Rio_Branco | UTC-5 (ACT) | AC e parte oeste do AM |
| America/Noronha | UTC-2 | Fernando de Noronha |
| America/Belem | UTC-3 | PA (deprecated, use America/Sao_Paulo) |
| America/Fortaleza | UTC-3 | CE, 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
| Critério | Tamanho | Tree-shakeable | Suporte a timezone |
|---|---|---|---|
| date-fns | ~70kb (mas tree-shakeable) | Sim, modular | Via @date-fns/tz |
| Luxon | ~70kb minificado | Não | Excelente, nativo |
| Day.js | ~7kb com plugins | Sim | Via plugin (timezone) |
| Moment.js | ~290kb | Não | Via moment-timezone (~70kb) |
| Date + Intl nativo | 0 (built-in) | — | Bom, via Intl.DateTimeFormat |
| Temporal API | 0 (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' }); // 330Quando 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
| Critério | O que usar |
|---|---|
| Banco de dados | TIMESTAMP WITH TIME ZONE (Postgres) ou TIMESTAMP (MySQL) — sempre UTC |
| API JSON | String ISO 8601 com Z final: '2026-04-18T14:30:00Z' |
| Em memória (JS) | Date (UTC interno) ou Temporal.ZonedDateTime |
| URL / query string | ISO 8601 URL-encoded ou Unix timestamp em segundos |
| Cache (Redis, etc.) | Unix timestamp em ms ou ISO 8601 — nunca string local |
| Logs | ISO 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
- Backend e banco em UTC. Configure timezone do servidor como UTC, mesmo em servidor brasileiro.
- API serializa em ISO 8601 com Z. Frontend recebe sempre UTC.
- Frontend usa Intl.DateTimeFormat com timeZone: 'America/Sao_Paulo' para exibir.
- Em forms, capture data + horário separados do timezone do usuário; converta para UTC antes de enviar.
- Logs sempre em UTC ISO, facilita correlação entre serviços em diferentes regiões.
- 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.
Continue lendo
Unix Timestamp (2026): O Que É, Como Funciona e Como Converter em Qualquer Linguagem
Guia completo do Unix timestamp: origem, cálculo, conversão em JavaScript, Python e SQL, fuso horário, Year 2038 problem e quando usar no lugar de datas formatadas.
JSON Schema: Como Validar e Documentar Dados (Guia Completo 2026)
JSON Schema é o padrão para validar estrutura de JSON em APIs e configs. Aprenda Draft 2020-12, validação com AJV e Pydantic, integração com OpenAPI/Swagger, geração de tipos TypeScript e comparativo com Zod, Yup e Joi.
DATETIME vs TIMESTAMP no Banco de Dados (Guia 2026): MySQL e PostgreSQL
Comparativo técnico completo: armazenamento, fuso horário, range, tamanho, comportamento em mudanças de TZ, TIMESTAMPTZ, problema 2038 e migração.
Performance SQL com Índices: Guia Completo 2026 (PostgreSQL e MySQL)
Aprenda a usar índices em SQL para acelerar queries de segundos para milissegundos. Tipos de índice (B-tree, GIN, GiST, BRIN), índices compostos, parciais, funcionais, EXPLAIN ANALYZE, anti-padrões e checklist de manutenção.