Artigo Build·Desenvolvimento·12 min de leitura

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.

Vitor Morais

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:

Temporal API vs Date nativo vs libs de terceiros
CritérioDate nativodate-fns + date-fns-tzTemporal API (Node 26)
ImutabilidadeNão — muta com setX()Sim (funções puras)Sim (tipos imutáveis)
Suporte a timezone IANANão nativo (depende de toLocaleString)Sim, via date-fns-tzSim, nativo no tipo
Aritmética de calendárioTransborda silenciosamenteSim, com addMonths()Sim, com overflow explícito
Bundle size (browser)0 KB~15–25 KB tree-shaken0 KB (nativo no runtime)
Dependência extraNenhumaSim — npm installNenhuma a partir do Node 26
Curva de aprendizadoBaixa (API familiar)Média (muitas funções)Média-alta (muitos tipos novos)
Suporte em browsers (2026)UniversalUniversalParcial — 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:

  1. Serialização e persistência. Temporal.ZonedDateTime.toString() gera uma string como 2026-06-10T09:00:00-03:00[America/Sao_Paulo]. Nem todo banco ou ORM parseia isso. Ao gravar, converta pra Instant (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.
  2. `Temporal.Now` não é mockável por padrão. Em testes, quem usa jest.useFakeTimers() controla Date.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.
  3. Conversão Date ↔ Temporal não é automática. Libs que retornam Date (drivers de banco, SDKs de terceiros) não viram Temporal.Instant sozinhas. O caminho é Temporal.Instant.fromEpochMilliseconds(date.getTime()) — funcional mas verboso.
  4. Performance em loops pesados. Criar milhares de Temporal.ZonedDateTime em loop (relatório com linha por minuto, por exemplo) pode ser mais lento que operar milissegundos crus com Date. Se a operação é bulk e sem exibição pra usuário, considerar manter timestamps numéricos e converter só na saída.
  5. Confusão entre Plain e Zoned. PlainDateTime parece completo (tem data e hora), mas não tem fuso. Comparar um PlainDateTime de São Paulo com um de Manaus não faz sentido — são relógios de parede sem referência absoluta. Usar ZonedDateTime ou Instant quando 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 fila

Repare 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:

  1. Isolar a camada de tempo. Criar um módulo time.ts que exporta funções como now(), fromEpoch(), toUserTimezone(). Internamente, migrar essas funções pra Temporal. O resto do código continua chamando as mesmas funções.
  2. Converter nas bordas. Na entrada (request do usuário, webhook, fila), converter pra Temporal.Instant ou ZonedDateTime. 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.
  3. 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.
  4. Remover date-fns/luxon só depois de cobertura. Quando todas as funções do módulo time.ts estiverem 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
#temporal-api-nodejs#nodejs-26#date-javascript#fusos-horarios#zoneddatetime#plaindate#instant-temporal#timezone-brasil#scheduling-nodejs#v8-14-6

Artigos relacionados