Temporal API no Node.js 26: como funciona e quando trocar o Date
Temporal API vem ativada por padrão no Node.js 26. Entenda calendar, ZonedDateTime e time zones — e quando Date nativo ainda resolve.
Por Vitor Morais
Fundador do MochaLabz ·
A Temporal API chega ativada por padrão no Node.js 26 — sem flag, sem polyfill, sem --harmony. O release de 5 de maio trouxe o motor V8 14.6 e, com ele, a implementação nativa de Temporal, a alternativa moderna ao objeto Date que o JavaScript arrasta há mais de duas décadas com problemas conhecidos: mutabilidade, ausência de suporte real a fusos horários e aritmética de datas que quebra em edge cases. A questão prática não é se vale aprender, mas quando Temporal resolve melhor que Date — e quando o overhead conceitual não compensa.
O que Temporal muda na prática em relação a Date
O Date do JavaScript representa um único ponto no tempo: milissegundos desde 1º de janeiro de 1970 UTC. Toda operação de fuso horário é delegada ao sistema operacional, e o resultado muda conforme o locale da máquina. Temporal desmembra esse conceito monolítico em tipos distintos, cada um com responsabilidade clara:
Temporal.Instant— ponto absoluto no tempo (equivalente a timestamp UTC), imutável.Temporal.PlainDate— data sem hora e sem fuso: "2026-05-11", ponto. Não muda se o servidor está em São Paulo ou Tokyo.Temporal.PlainTime— hora sem data e sem fuso: "14:30:00".Temporal.PlainDateTime— data + hora, mas ainda sem fuso. Representa um "relógio de parede".Temporal.ZonedDateTime— data + hora + fuso horário explícito. É o tipo que resolve a maioria dos bugs reais de timezone.Temporal.Duration— intervalo de tempo ("2 horas e 30 minutos"), com aritmética que respeita meses de tamanho diferente.
A diferença mais importante: todos os tipos Temporal são imutáveis. Chamar .add() ou .subtract() retorna um novo objeto. Com Date, chamar setMonth() muta o objeto original — fonte histórica de bugs silenciosos em scheduling, geração de relatórios e qualquer lógica que reutilize a mesma instância.
ZonedDateTime vs toLocaleString: fusos no Brasil sem gambiarras
O Brasil opera com quatro fusos horários oficiais e, dependendo do estado, horário de verão pode ou não existir (foi abolido em 2019 mas pode voltar). Quem precisa agendar um job, gerar um recibo com hora local ou exibir deadline pro usuário final enfrenta dois problemas com Date: o construtor assume UTC ou o locale da máquina, e converter entre fusos exige bibliotecas como date-fns-tz ou luxon.
Temporal.ZonedDateTime resolve isso nativamente. O fuso é parte do tipo, não uma string passada pra função de formatação. Quem lida com fusos horários em JavaScript conhece as dores de offset manualmente calculado — Temporal elimina essa camada.
Criar data/hora em fuso brasileiro com Temporal
// Agendar cobrança para 10 de junho, 9h, horário de Brasília
const cobranca = Temporal.ZonedDateTime.from({
year: 2026,
month: 6,
day: 10,
hour: 9,
minute: 0,
timeZone: 'America/Sao_Paulo',
});
console.log(cobranca.toInstant().epochMilliseconds);
// => timestamp absoluto, correto independente do servidor
console.log(cobranca.toString());
// => '2026-06-10T09:00:00-03:00[America/Sao_Paulo]'Mesmo cenário com Date — a dor
// Abordagem clássica: construir em UTC e subtrair 3 horas
const d = new Date('2026-06-10T09:00:00-03:00');
// Parece funcionar, mas:
// 1. Se o servidor está em UTC+0, d.getHours() retorna 12, não 9.
// 2. Se o Brasil voltar a ter horário de verão, o offset muda
// e o código não sabe.
// 3. d é mutável — qualquer setHours() em outro lugar altera d.A vantagem não é estética. É que Temporal.ZonedDateTime carrega o IANA timezone database junto, então a conversão entre America/Sao_Paulo e America/Manaus (fuso -4h sem horário de verão) funciona sem lookup manual. Quem persiste timestamps no banco de dados pode converter pra Temporal.Instant antes de salvar e reconstruir o ZonedDateTime na leitura.
Aritmética de datas: onde Temporal evita bugs reais
Somar 1 mês a 31 de janeiro com Date é um clássico: o resultado é 3 de março (porque fevereiro não tem 31 dias e Date transborda silenciosamente). Temporal.PlainDate lança erro ou resolve com overflow controlado:
Somar meses sem surpresas
const jan31 = Temporal.PlainDate.from('2026-01-31');
// Comportamento padrão: "constrain" — ajusta pro último dia válido
const fev = jan31.add({ months: 1 });
console.log(fev.toString()); // '2026-02-28'
// Se quiser erro explícito em vez de ajuste silencioso:
try {
jan31.add({ months: 1 }, { overflow: 'reject' });
} catch (e) {
console.log('Overflow rejeitado — data inválida');
}Duration também resolve a ambiguidade de "1 mês". Com Date, somar 30 dias não é a mesma coisa que somar 1 mês (março tem 31 dias). Temporal.Duration distingue { months: 1 } de { days: 30 } — e a aritmética respeita o calendário.
Quando Date nativo ainda resolve (e Temporal é excesso)
Nem todo código que toca em data precisa migrar. Para operações simples e UTC-only, Date continua funcional e mais direto:
- Timestamp de log:
Date.now()retorna milissegundos UTC. Funciona, é rápido, não precisa de Temporal. - Comparação simples entre dois pontos:
dateA.getTime() > dateB.getTime()resolve.Temporal.Instant.compare()faz o mesmo com mais cerimônia. - Exibição formatada sem fuso:
toISOString()gera ISO 8601 e é suficiente para APIs internas que só trafegam UTC. - Scripts pontuais e CLIs: se o código roda uma vez, no mesmo fuso, e não persiste resultado, a complexidade extra de Temporal não paga.
Regra prática pra decidir
Se o código precisa lidar com mais de um fuso horário, aritmética de meses/anos ou imutabilidade garantida, Temporal resolve problemas reais. Se é timestamp UTC puro ou comparação de milissegundos, Date.now() é suficiente e mais enxuto.
Comparação direta: Temporal vs Date vs date-fns/luxon
Até agora, quem queria timezone handling decente em Node.js dependia de libs externas. Com Temporal nativo no Node 26, a equação muda:
| Critério | Date nativo | date-fns + date-fns-tz | Temporal API (Node 26) |
|---|---|---|---|
| Imutabilidade | Não — muta com setX() | Sim (funções puras) | Sim (tipos imutáveis) |
| Suporte a timezone IANA | Não nativo (depende de toLocaleString) | Sim, via date-fns-tz | Sim, nativo no tipo |
| Aritmética de calendário | Transborda silenciosamente | Sim, com addMonths() | Sim, com overflow explícito |
| Bundle size (browser) | 0 KB | ~15–25 KB tree-shaken | 0 KB (nativo no runtime) |
| Dependência extra | Nenhuma | Sim — npm install | Nenhuma a partir do Node 26 |
| Curva de aprendizado | Baixa (API familiar) | Média (muitas funções) | Média-alta (muitos tipos novos) |
| Suporte em browsers (2026) | Universal | Universal | Parcial — ainda atrás de flag em alguns |
Temporal no browser ainda não é universal
No Node.js 26, Temporal roda sem flag. Nos browsers, o suporte varia — Chrome/V8 está avançado, mas Safari e Firefox podem exigir polyfill. Se o código roda só no servidor (API routes, cronjobs, scripts), Temporal é seguro. Se roda no browser, verifique compatibilidade antes de remover o date-fns.
Armadilhas ao adotar Temporal em produção
A API é bem desenhada, mas tem ciladas que aparecem em uso real:
- Serialização e persistência.
Temporal.ZonedDateTime.toString()gera uma string como2026-06-10T09:00:00-03:00[America/Sao_Paulo]. Nem todo banco ou ORM parseia isso. Ao gravar, converta praInstant(epoch millis ou ISO 8601 UTC) e persista o timezone como coluna separada, se necessário. Essa lógica é parecida com a decisão entre DATETIME e TIMESTAMP em bancos relacionais. - `Temporal.Now` não é mockável por padrão. Em testes, quem usa
jest.useFakeTimers()controlaDate.now().Temporal.Now.instant()não é interceptado pela mesma API. Isolar a chamada num módulo próprio (wrapper injetável) resolve, mas exige refactor. - Conversão Date ↔ Temporal não é automática. Libs que retornam
Date(drivers de banco, SDKs de terceiros) não viramTemporal.Instantsozinhas. O caminho éTemporal.Instant.fromEpochMilliseconds(date.getTime())— funcional mas verboso. - Performance em loops pesados. Criar milhares de
Temporal.ZonedDateTimeem loop (relatório com linha por minuto, por exemplo) pode ser mais lento que operar milissegundos crus comDate. Se a operação é bulk e sem exibição pra usuário, considerar manter timestamps numéricos e converter só na saída. - Confusão entre Plain e Zoned.
PlainDateTimeparece completo (tem data e hora), mas não tem fuso. Comparar umPlainDateTimede São Paulo com um de Manaus não faz sentido — são relógios de parede sem referência absoluta. UsarZonedDateTimeouInstantquando precisar comparar entre fusos.
Exemplo real: agendar job em fuso brasileiro e converter pra UTC
Um cenário comum: um sistema precisa disparar e-mail de cobrança às 9h no horário do cliente, que está em São Paulo. O servidor roda em UTC (Vercel, Cloudflare Workers, qualquer cloud). Com Temporal, o código fica explícito e auditável:
Agendar job com timezone explícito
function proximaCobranca(timezone: string): number {
const agora = Temporal.Now.zonedDateTimeISO(timezone);
// Próximo dia útil às 9h (simplificado — sem feriados)
let alvo = agora.with({ hour: 9, minute: 0, second: 0 });
// Se já passou das 9h hoje, vai pro dia seguinte
if (Temporal.ZonedDateTime.compare(alvo, agora) <= 0) {
alvo = alvo.add({ days: 1 });
}
// Pula sábado (6) e domingo (7)
while (alvo.dayOfWeek === 6 || alvo.dayOfWeek === 7) {
alvo = alvo.add({ days: 1 });
}
// Retorna epoch millis pra agendar no cron/queue
return alvo.toInstant().epochMilliseconds;
}
const ms = proximaCobranca('America/Sao_Paulo');
console.log(new Date(ms).toISOString());
// => data/hora em UTC, pronta pra gravar no banco ou na filaRepare que dayOfWeek retorna 1–7 (segunda a domingo), diferente de Date.getDay() que retorna 0–6 (domingo a sábado). Essa mudança de convenção é um ponto de atenção na migração. Quem já trabalha com Unix timestamps vai notar que o epochMilliseconds mantém compatibilidade direta com o ecossistema existente.
Calendar system: existe, mas raramente importa
Temporal suporta calendários não-gregorianos (hebraico, islâmico, japonês) via Temporal.Calendar. Na prática, 99% dos projetos em produção no Brasil usam 'iso8601' (gregoriano padrão) e nunca tocam nesse parâmetro. Ele existe, é passado implicitamente, e pode ser ignorado sem consequências — a menos que o produto atenda mercados com calendários distintos.
Como migrar gradualmente sem reescrever tudo
Trocar todo Date por Temporal de uma vez é receita pra quebrar coisas. Uma migração pragmática funciona em camadas:
- Isolar a camada de tempo. Criar um módulo
time.tsque exporta funções comonow(),fromEpoch(),toUserTimezone(). Internamente, migrar essas funções pra Temporal. O resto do código continua chamando as mesmas funções. - Converter nas bordas. Na entrada (request do usuário, webhook, fila), converter pra
Temporal.InstantouZonedDateTime. Na saída (resposta JSON, gravação no banco), converter de volta pra ISO string ou epoch millis. O core da aplicação opera com tipos Temporal. - Manter Date nos testes que já funcionam. Não refatorar suíte de testes que está verde só pra usar Temporal. Adicionar testes novos com Temporal nas funções migradas.
- Remover date-fns/luxon só depois de cobertura. Quando todas as funções do módulo
time.tsestiverem com Temporal e testes passando, remover a dependência de terceiros. Até lá, as duas coexistem.
Essa abordagem evita big bang e permite reverter um arquivo de cada vez se algo quebrar.
Perguntas frequentes
Temporal API já funciona no Node.js 26 sem flag?+
Sim. O Node.js 26, lançado em 5 de maio de 2026, ativa Temporal por padrão via V8 14.6. Não precisa de `--harmony-temporal` nem de polyfill. Basta atualizar o Node e usar `Temporal.Now`, `Temporal.PlainDate`, `Temporal.ZonedDateTime` etc. diretamente.
Temporal substitui date-fns e luxon?+
Para código server-side no Node 26, sim na maioria dos casos — timezone handling, aritmética de datas e imutabilidade são nativos. No browser, o suporte ainda é parcial em 2026, então date-fns pode continuar necessário no front-end até os engines convergirem.
Como converter Date pra Temporal e vice-versa?+
De Date pra Temporal: `Temporal.Instant.fromEpochMilliseconds(date.getTime())`. De Temporal pra Date: `new Date(instant.epochMilliseconds)`. A conversão é manual mas direta — não existe cast automático entre os dois.
Temporal é mais lento que Date?+
Para operações simples como capturar timestamp (`Date.now()` vs `Temporal.Now.instant()`), a diferença é negligível. Em loops com milhares de criações de `ZonedDateTime`, pode haver overhead mensurável. Para a maioria das aplicações — APIs, cronjobs, lógica de negócio — a diferença de performance não justifica evitar Temporal.
Temporal.PlainDate e Temporal.ZonedDateTime: qual usar?+
`PlainDate` é para datas sem contexto de fuso — aniversário, data de vencimento genérica, feriado fixo. `ZonedDateTime` é para momentos que dependem de onde o usuário está — horário de reunião, deadline de pagamento, disparo de notificação. Se precisa comparar horários entre locais diferentes, `ZonedDateTime` ou `Instant`.
Converter timestamps entre formatos
Converta Unix timestamps pra data legível e vice-versa — útil pra conferir o epochMilliseconds que Temporal retorna.
Abrir conversor de timestamp →Artigos relacionados
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.
Fusos Horários em JavaScript: Guia Completo (2026) com Date, Intl e Temporal
Como Date funciona internamente, Intl.DateTimeFormat por região, timezones do Brasil, parsing seguro, horário de verão, a nova API Temporal e bibliotecas como date-fns e Luxon.
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.
Node.js 27 LTS: novo schedule e o que muda no seu CLI
Node.js muda para um release major por ano. Veja como adaptar seu CLI ou SaaS ao novo schedule de LTS, evitar breaking changes e planejar suporte.