Aprenda a formatar e converter tempo em JavaScript sem surpresas: timestamps, strings ISO, fusos horários, DST, regras de parsing e padrões confiáveis.

Bugs relacionados a tempo em JavaScript raramente parecem um “relógio errado”. Eles aparecem como pequenos deslocamentos confusos: uma data que está correta no seu laptop mas errada na máquina de um colega, uma resposta de API que parece ok até ser renderizada em outro fuso, ou um relatório que fica “um dia a menos” perto de uma mudança sazonal.
Normalmente você vai notar um (ou mais) destes:
+02:00) é diferente do esperado.Uma grande fonte de dor é que a palavra tempo pode se referir a conceitos distintos:
O Date nativo do JavaScript tenta cobrir todos esses casos, mas representa primariamente um instante no tempo enquanto empurra você constantemente para a exibição local, o que facilita conversões acidentais.
Este guia é intencionalmente prático: como obter conversões previsíveis entre navegadores e servidores, como escolher formatos mais seguros (como ISO 8601) e como identificar armadilhas clássicas (segundos vs milissegundos, UTC vs local e diferenças de parsing). O objetivo não é mais teoria — é menos surpresas do tipo “por que mudou?”.
Bugs de tempo em JavaScript frequentemente começam ao misturar representações que parecem intercambiáveis, mas não são.
1) Epoch em milissegundos (number)
Um número simples como 1735689600000 normalmente significa “milissegundos desde 1970-01-01T00:00:00Z”. Representa um instante no tempo sem formatação ou fuso.
2) Objeto Date (invólucro de um instante)
Um Date armazena o mesmo tipo de instante que um timestamp. A parte confusa: quando você imprime um Date, o JavaScript o formata usando as regras locais do ambiente, a menos que você peça explicitamente o contrário.
3) String formatada (exibição humana)
Strings como "2025-01-01", "01/01/2025 10:00" ou "2025-01-01T00:00:00Z" não são todas a mesma coisa. Algumas são unambiguamente ISO (com Z), outras dependem da localidade, e algumas não incluem fuso horário.
O mesmo instante pode aparecer diferente conforme o fuso:
const instant = new Date("2025-01-01T00:00:00Z");
instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"
instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (dia anterior)
Escolha uma representação interna e consistente (comum: epoch em milissegundos ou ISO 8601 em UTC) e mantenha-a em toda a app e APIs. Converta para/desde Date e strings formatadas apenas nas bordas: parsing de entrada e exibição na UI.
“Timestamp” normalmente significa tempo Unix: a contagem desde 1970-01-01 00:00:00 UTC. O detalhe: sistemas diferentes usam unidades diferentes.
O Date do JavaScript é a origem de muita confusão porque usa milissegundos. Muitas APIs, bancos e logs usam segundos.
17040672001704067200000Mesmo momento, mas a versão em milissegundos tem três dígitos a mais.
Use multiplicação/divisão explícita:
// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);
// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);
// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);
// Date -> milliseconds
const msOut = d2.getTime();
Date()Parece razoável, mas está errado quando ts está em segundos:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
O resultado será uma data em 1970, porque 1.704.067.200 milissegundos é apenas cerca de 19 dias depois do epoch.
Quando não tiver certeza da unidade, adicione guardrails simples:
function asDateFromUnknownEpoch(x) {
// heurística: segundos ~1e9-1e10, milissegundos ~1e12-1e13
if (x < 1e11) return new Date(x * 1000); // assume seconds
return new Date(x); // assume milliseconds
}
const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());
Se a contagem de “dígitos” for ~10, provavelmente são segundos. Se for ~13, provavelmente milissegundos. Imprima toISOString() ao depurar: é inequívoco e ajuda a detectar erros de unidade imediatamente.
O Date do JavaScript pode confundir porque armazena um instante único, mas pode apresentar esse instante em fusos diferentes.
Internamente, um Date é essencialmente “milissegundos desde o epoch Unix (1970-01-01T00:00:00Z)”. Esse número representa um momento em UTC. O “deslocamento” ocorre quando você pede ao JavaScript para formatar esse momento como hora local (com base nas configurações da máquina) versus UTC.
Muitas APIs de Date têm variantes local e UTC. Elas retornam números diferentes para o mesmo instante:
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // hora em *fuso local*
d.getUTCHours(); // hora em UTC
d.toString(); // string em hora local
d.toISOString(); // UTC (sempre termina com Z)
Se sua máquina está em Nova York (UTC-5), aquele horário UTC pode aparecer como “19:30” do dia anterior localmente. Em um servidor configurado para UTC, aparecerá como “00:30”. Mesmo instante, exibição diferente.
Logs frequentemente usam Date#toString() ou interpolam um Date implicitamente, que usa o fuso local do ambiente. Isso significa que o mesmo código pode imprimir timestamps diferentes no seu laptop, na CI e na produção.
Armazene e transmita tempo em UTC (ex.: epoch ms ou ISO 8601 com Z). Converta para fuso do usuário apenas na exibição:
toISOString() ou passe epoch msIntl.DateTimeFormatSe você está construindo uma app rapidamente, vale a pena definir isso já nos contratos de API: nomeie campos claramente (createdAtMs, createdAtIso) e mantenha server (Go + PostgreSQL) e client (React) alinhados sobre o que cada campo representa.
Se precisa enviar datas/horários entre browser, servidor e banco, strings ISO 8601 são a escolha padrão mais segura. São explícitas, amplamente suportadas e (o mais importante) carregam informação de fuso.
Dois bons formatos para troca:
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00O que significa o “Z”?
Z é Zulu, outro nome para UTC. Então 2025-03-04T12:30:00Z é “12:30 em UTC”.
Quando o offset importa?
Offsets são cruciais quando um evento está atrelado a um contexto local (compromissos, reservas, horário de funcionamento). 2025-03-04T12:30:00+02:00 descreve um momento que está duas horas à frente de UTC, e não é o mesmo instante que 2025-03-04T12:30:00Z.
Strings como 03/04/2025 são uma armadilha: é 3 de março ou 4 de março? Usuários e ambientes interpretam de formas diferentes. Prefira 2025-03-04 (ISO date) ou um datetime ISO completo.
const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();
console.log(iso); // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z
Esse “round-trip” é exatamente o que você quer para APIs: consistente, previsível e consciente de fuso.
Date.parse() parece conveniente: dá uma string, recebe um timestamp. O problema é que, para qualquer coisa que não seja claramente ISO 8601, o parsing pode depender de heurísticas do navegador. Essas heurísticas já variaram entre motores e versões, então a mesma entrada pode ser parseada diferente (ou nem parseada) dependendo de onde o código roda.
Date.parse() pode variarJavaScript só padroniza parsing de forma confiável para strings no estilo ISO 8601 (e mesmo aí detalhes como fuso importam). Para formatos “amigáveis” — como "03/04/2025", "March 4, 2025" ou "2025-3-4" — os navegadores podem interpretar:
Se você não consegue prever a forma exata da string, não consegue prever o resultado.
YYYY-MM-DDUma armadilha comum é a forma "YYYY-MM-DD" (por exemplo, "2025-01-15"). Muitos desenvolvedores esperam que seja interpretada como meia-noite local. Na prática, alguns ambientes tratam essa forma como meia-noite em UTC.
Essa diferença importa: meia-noite em UTC convertida para hora local pode virar o dia anterior em fusos negativos (ex.: Américas) ou deslocar a hora inesperadamente. É uma maneira fácil de cair no bug “por que minha data está um dia fora?”.
Para entrada de servidor/API:
2025-01-15T13:45:00Z ou 2025-01-15T13:45:00+02:00."YYYY-MM-DD") e evite converter para Date a menos que também defina o fuso pretendido.Para entrada de usuário:
03/04/2025 a menos que sua UI force o significado.Em vez de confiar em Date.parse() para “descobrir”, escolha um destes padrões:
new Date(year, monthIndex, day) para datas locais).Quando dados de tempo são críticos, “funciona na minha máquina” não é suficiente — torne suas regras de parsing explícitas e consistentes.
Se o objetivo é “mostrar uma data/hora do jeito que as pessoas esperam”, a melhor ferramenta em JavaScript é Intl.DateTimeFormat. Ela usa regras de localidade do usuário (ordem, separadores, nomes de meses) e evita a abordagem frágil de concatenar strings manualmente como month + '/' + day.
Formatação manual muitas vezes hardcoda saída no estilo americano, esquece zeros à esquerda, ou produz resultados 24/12 horas conflitantes. Intl.DateTimeFormat também deixa explícito em qual fuso você está exibindo — crítico quando seus dados estão em UTC mas a UI deve mostrar a hora local do usuário.
Para “formatar bonitinho”, dateStyle e timeStyle são as mais simples:
const d = new Date('2025-01-05T16:30:00Z');
// Local do usuário + fuso local do usuário
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Forçar um fuso específico (ótimo para horários de evento)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
Se precisar de ciclo de hora consistente (por exemplo, opção de configurações), use hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Escolha uma função de formatação por “tipo” de timestamp na sua UI (horário de mensagem, log, início de evento) e mantenha a decisão de timeZone intencional:
Isso dá saída consistente e amigável sem precisar manter um conjunto frágil de strings de formatação customizadas.
O Horário de Verão (DST) é quando um fuso ajusta seu offset em relação ao UTC (tipicamente uma hora) em datas específicas. A parte traiçoeira: DST não apenas “muda o offset” — ele altera a existência de certos horários locais.
Quando os relógios adiantam, uma faixa de horários locais não acontece. Por exemplo, em muitas regiões o relógio pula de 01:59 para 03:00, então 02:30 horário local é “inexistente”.
Quando os relógios atrasam, uma faixa de horários locais acontece duas vezes. Por exemplo, 01:30 pode ocorrer uma vez antes da mudança e outra depois, significando que o mesmo horário de relógio pode referir-se a dois instantes diferentes.
Esses não são equivalentes ao redor de fronteiras de DST:
Se o DST começa hoje, “amanhã às 9:00” pode estar apenas 23 horas longe. Se o DST termina, pode estar 25 horas longe.
// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.
setHours pode surpreenderSe você fizer date.setHours(2, 30, 0, 0) em um dia de “spring forward”, o JavaScript pode normalizar para um horário válido diferente (frequentemente 03:30), porque 02:30 não existe em hora local.
setDate) em vez de somar milissegundos.Z para que o instante seja inequívoco.Uma fonte comum de bugs é usar Date para algo que não é um momento de calendário.
Um timestamp responde “quando isso aconteceu?” (um instante específico como 2025-12-23T10:00:00Z). Uma duração responde “por quanto tempo?” (como “3 minutos 12 segundos”). Conceitos diferentes — misturá-los gera matemática confusa e efeitos inesperados de fuso/DST.
Date é a ferramenta errada para duraçõesDate sempre representa um ponto na linha do tempo relativo a um epoch. Se você armazenar “90 segundos” como um Date, na prática está guardando “1970-01-01 mais 90 segundos” em um fuso. Formatá-lo pode mostrar 01:01:30, deslocar uma hora ou adicionar uma data que você não quis.
Para durações, prefira números puros:
HH:mm:ssUm formatador simples para timers ou durações de mídia:
function formatHMS(totalSeconds) {
const s = Math.max(0, Math.floor(totalSeconds));
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
formatHMS(75); // "00:01:15" (countdown timer)
formatHMS(5423); // "01:30:23" (media duration)
Se estiver convertendo a partir de minutos, multiplique primeiro (minutes * 60) e mantenha o valor numérico até a renderização.
Ao comparar tempos em JavaScript, o mais seguro é comparar números, não texto formatado. Um objeto Date é essencialmente um invólucro sobre um timestamp numérico (epoch ms), então você quer que as comparações virem “número vs número”.
Use getTime() (ou Date.valueOf(), que retorna o mesmo número) para comparar de forma confiável:
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a é anterior
}
// Também funciona:
if (+a < +b) {
// o + unário chama valueOf()
}
Evite comparar strings formatadas como "1/10/2025, 12:00 PM" — são dependentes de localidade e não ordenam corretamente. A exceção principal são strings ISO 8601 no mesmo formato e fuso (ex.: todas terminando com ...Z), que ordenam lexicograficamente.
Ordenar por tempo é simples se você ordenar por epoch ms:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Filtrar itens dentro de um intervalo é a mesma ideia:
const start = new Date('2025-01-01T00:00:00Z').getTime();
const end = new Date('2025-02-01T00:00:00Z').getTime();
const inRange = items.filter(i => {
const t = new Date(i.createdAt).getTime();
return t >= start && t < end;
});
“Início do dia” depende se você quer hora local ou UTC:
// Início/fim do dia local
const d = new Date(2025, 0, 10); // 10 Jan em hora local
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
// Início/fim do dia em UTC
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));
Escolha uma definição cedo e mantenha-a em todas as comparações e lógica de intervalo.
Bugs de tempo parecem aleatórios até você identificar o que você tem (timestamp? string? Date?) e onde o deslocamento é introduzido (parsing, conversão de fuso, formatação).
Comece registrando o mesmo valor de três maneiras diferentes. Isso revela rapidamente se o problema é segundos vs milissegundos, local vs UTC, ou parsing de string.
console.log('raw input:', input);
const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());
O que observar:
toISOString() estiver muito fora (ex.: ano 1970 ou futuro distante), suspeite de segundos vs milissegundos.toISOString() parece certo mas toString() está “deslocado”, você está vendo uma questão de exibição em fuso local.getTimezoneOffset() muda dependendo da data, você está cruzando horário de verão.Muitos relatórios “funciona na minha máquina” são apenas padrões de ambiente diferentes.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Se seu servidor roda em UTC mas seu laptop roda em um fuso local, a saída formatada difere a menos que você especifique timeZone explicitamente.
Crie testes unitários em torno das bordas de DST e horários extremos:
23:30 → 00:30Se você iterate rápido, considere incluir esses testes no scaffolding. Por exemplo, ao gerar uma app React + Go, adicione uma pequena suíte de “contrato de tempo” (exemplos de payload + asserts de parsing/formatação) para pegar regressões antes do deploy.
"2025-03-02 10:00" sem fuso.locale e (quando necessário) timeZone.Manipulação confiável de tempo em JavaScript é, na maior parte, escolher uma “fonte de verdade” e ser consistente do armazenamento até a exibição.
Armazene e calcule em UTC. Trate hora local do usuário como detalhe de apresentação.
Transmita datas entre sistemas como strings ISO 8601 com offset explícito (preferivelmente Z). Se mandar epochs numéricos, documente a unidade e mantenha consistente (milissegundos é o padrão comum em JS).
Formate para humanos com Intl.DateTimeFormat (ou toLocaleString), e passe timeZone quando precisar de saída determinística (por exemplo, sempre mostrar em UTC ou em uma região de negócios específica).
Z (ex.: 2025-12-23T10:15:00Z). Se usar epochs, nomeie campos como createdAtMs para deixar a unidade óbvia.Considere uma biblioteca de data-hora dedicada se precisar de eventos recorrentes, regras complexas de fuso, aritmética segura em DST (“mesmo horário local amanhã”) ou muito parsing de entradas inconsistentes. O valor está em APIs mais claras e menos bugs de casos extremos.
Se quiser se aprofundar, veja mais guias sobre tempo em /blog. Se estiver avaliando ferramentas ou suporte, veja /pricing.