A data voltou um dia

Você salvou 2026-08-16 e a tela mostra 15. Não é bug do banco — é a regra do new Date, e ela muda com o formato da string.

Roda no seu navegador · nada é enviado para servidor

Cole a string de data

A regra que causa quase tudo

O JavaScript trata duas strings quase iguais de formas opostas, e está na especificação:

StringInterpretada comoNo Brasil vira
'2026-08-16' UTC — meia-noite em Greenwich 15/08 às 21h
'2026-08-16T00:00:00' horário local 16/08 às 00h
'2026-08-16T00:00:00Z' UTC, explícito 15/08 às 21h
'2026-08-16T00:00:00-03:00' o fuso que você escreveu 16/08 às 00h

Repare: só de tirar o horário da string, o significado muda de local para UTC. É a origem do “sumiu um dia” — a data que veio do banco como 2026-08-16 vira 15 de agosto às nove da noite, e getDate() devolve 15.

Isso não acontece só no Brasil, mas dói mais aqui: como estamos a oeste de Greenwich, nosso relógio fica atrás, e a meia-noite UTC cai no dia anterior. A leste — Lisboa, Tóquio — o erro aparece ao contrário, e às vezes nem aparece.

O segundo culpado: toISOString()

Este fecha o ciclo do bug. O toISOString() sempre converte para UTC, então o padrão mais comum de salvar data no Brasil está errado:

// usuário escolheu 16/08 no seletor, às 20h de Brasília
const d = new Date(2026, 7, 16)          // 16/08 00:00 local
d.toISOString()                          // '2026-08-16T03:00:00.000Z'
d.toISOString().slice(0, 10)             // '2026-08-16'  ← funcionou

// mas se a hora local for antes das 21h... na verdade:
const e = new Date('2026-08-16')         // interpretada como UTC!
e.toISOString().slice(0, 10)             // '2026-08-16'
e.getDate()                              // 15  ← aqui aparece

A regra prática: toISOString().slice(0,10) devolve o dia em Greenwich, não o dia de quem escolheu. Para pegar a data local:

// pt-BR devolve dd/mm/aaaa; en-CA devolve aaaa-mm-dd, que é o que você quer
const dataLocal = d.toLocaleDateString('en-CA')     // '2026-08-16'

// ou explícito, com o fuso que interessa
new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Sao_Paulo' }).format(d)

A pegadinha do getTimezoneOffset()

O sinal é invertido em relação ao que todo mundo espera. No Brasil, que é UTC−3, ele devolve 180 — positivo:

new Date().getTimezoneOffset()   // 180 em Brasília, não -180

O valor responde “quantos minutos preciso somar ao horário local para chegar no UTC”. Faz sentido para a conta, e trai quem lê rápido.

A distinção que resolve o problema de vez

Quase todo bug de fuso vem de misturar duas coisas diferentes:

Instante é um ponto na linha do tempo, igual para todo mundo: quando o pedido foi criado, quando o login aconteceu. Guarde em UTC, converta só na hora de mostrar.

Data de calendário não é um instante: data de nascimento, vencimento de boleto, feriado. O aniversário de quem nasceu em 16 de agosto é 16 de agosto no mundo inteiro — não existe “meia-noite de qual fuso”.

InstanteData de calendário
Exemploscriado_em, loginnascimento, vencimento
Coluna no MySQLDATETIME em UTCDATE
No JavaScriptDatestring '2026-08-16', sem virar Date
Erro típicoguardar no fuso localconverter para UTC

Guardar data de nascimento como timestamp é o caminho garantido para o aniversário andar um dia dependendo de onde a pessoa abre o sistema.

O Brasil não tem mais horário de verão — mas o histórico tem

O horário de verão foi extinto em 2019. Hoje o país é −03:00 fixo (com −04:00 e −05:00 em parte do Norte e Oeste).

Só que datas anteriores a 2019 continuam sujeitas ao antigo horário de verão. Se o seu sistema tem histórico, uma data de dezembro de 2018 estava em −02:00, e é por isso que se calcula fuso pelo nome da região, nunca por um número fixo:

// certo — a base de fusos sabe o histórico
new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo' })

// errado — quebra em qualquer data anterior a 2019
const dataLocal = new Date(utc.getTime() - 3 * 60 * 60 * 1000)

Como testar antes de o usuário reclamar

Bug de fuso quase nunca aparece para quem programa, porque a máquina de desenvolvimento costuma estar no mesmo fuso do teste. Force outro:

# roda a suíte como se você estivesse em outro lugar
TZ=Pacific/Kiritimati npm test     # UTC+14, o mais adiantado do mundo
TZ=Pacific/Midway     npm test     # UTC-11, o mais atrasado
TZ=UTC                npm test

Se a suíte passa nos três, o código não depende do fuso da máquina — que é a única garantia que vale.

Outras ferramentas de diagnóstico: faixas do semver, acentuação quebrada, erro de npm.

Newsletter

Receba os artigos novos por e-mail

Sem spam. Só o aviso quando sai um tutorial ou artigo novo.