Datas e fuso horário em JavaScript: por que sua data muda de dia
Datas mudam de dia quando o código mistura dois conceitos: uma data do calendário, como um aniversário, e um instante no tempo, como a hora de um pedido. Este guia explica por que new Date("2026-01-15") vira dia 14 no Brasil, como mostrar qualquer instante no horário de Brasília e como evitar os erros de fuso mais comuns em JavaScript, Python e PHP.
Data do calendário ou instante no tempo?
- Data do calendário: aniversário, vencimento de boleto, feriado. É "15 de janeiro" em qualquer lugar do mundo e não tem hora nem fuso.
- Instante no tempo: quando o pedido foi feito, quando a mensagem chegou. É um ponto único no tempo, que aparece com horas diferentes em cada fuso.
O Date do JavaScript só representa instantes. Quando você o usa para uma data do calendário, ele escolhe uma hora e um fuso por você, e é daí que vêm os erros. Para ver um mesmo instante em UTC e no seu fuso, use o conversor de timestamp.
Por que new Date("2026-01-15") vira dia 14
Pela regra do JavaScript, um texto só com a data ("2026-01-15") é lido como meia-noite em UTC. No Brasil, três horas atrás, esse instante ainda é dia 14, às 21h. Já um texto com data e hora sem fuso ("2026-01-15T00:00") é lido no horário local. A diferença de comportamento entre os dois formatos é a origem do erro.
O caminho inverso tem a mesma armadilha: new Date(2026, 0, 15).toISOString() devolve "2026-01-15T03:00:00.000Z" no Brasil. Para mandar uma data do calendário para uma API, mande o texto "2026-01-15", não o resultado do toISOString.
// "2026-01-15" sem hora é lido como meia-noite em UTC. No Brasil (UTC-3), isso
// ainda é dia 14, às 21h. Para uma data de calendário, monte com ano, mês e dia:
function dataDoCalendario(texto) {
const [ano, mes, dia] = texto.split('-').map(Number);
return new Date(ano, mes - 1, dia); // mês começa em 0
}
// Mostra o instante no horário de Brasília, qualquer que seja o fuso da máquina
function formatarEmBrasilia(data) {
return new Intl.DateTimeFormat('pt-BR', {
timeZone: 'America/Sao_Paulo',
dateStyle: 'short',
timeStyle: 'short',
}).format(data);
} O mês do Date começa em 0: janeiro é 0, dezembro é 11.
Mostrar no horário de Brasília
O Intl.DateTimeFormat com a opção timeZone: "America/Sao_Paulo" mostra o instante no horário de Brasília, seja o fuso do servidor UTC, seja o do navegador de alguém no Japão. Brasília está em UTC-3 o ano todo desde 2019, quando o horário de verão acabou.
O Brasil tem outros fusos: America/Manaus (UTC-4, Amazonas), America/Rio_Branco (UTC-5, Acre) e America/Noronha (UTC-2). Se o sistema atende o país todo, use o fuso de quem vê a página, que o navegador já informa.
Em Python e PHP
No Python, datetime.now() sem argumento devolve a hora do servidor, que em nuvem costuma ser UTC. Passe o fuso (ZoneInfo) e use date para datas do calendário, que não têm fuso. No PHP, o fuso padrão vem do php.ini; deixe o fuso explícito no código.
from datetime import date, datetime
from zoneinfo import ZoneInfo # no Windows: pip install tzdata
BRASILIA = ZoneInfo("America/Sao_Paulo")
def agora_em_brasilia() -> datetime:
# datetime.now() sem fuso devolve a hora do servidor, que costuma estar em UTC
return datetime.now(BRASILIA)
def formatar_em_brasilia(instante: datetime) -> str:
if instante.tzinfo is None:
raise ValueError("Use um datetime com fuso, para não depender do servidor")
return instante.astimezone(BRASILIA).strftime("%d/%m/%Y %H:%M")
def data_do_calendario(texto: str) -> date:
return date.fromisoformat(texto) # date não tem hora nem fuso: 15 é sempre 15 <?php
function formatarEmBrasilia(DateTimeInterface $instante): string
{
return DateTimeImmutable::createFromInterface($instante)
->setTimezone(new DateTimeZone('America/Sao_Paulo'))
->format('d/m/Y H:i');
}
function dataDoCalendario(string $texto): DateTimeImmutable
{
// O ! zera a hora; o fuso explícito evita depender do date.timezone do php.ini
$fuso = new DateTimeZone('America/Sao_Paulo');
return DateTimeImmutable::createFromFormat('!Y-m-d', $texto, $fuso);
} Regras que evitam a maior parte dos erros
- Guarde instantes em UTC (no banco, como
timestamptzno PostgreSQL ou como timestamp Unix) e converta para o fuso só na hora de mostrar. - Guarde datas do calendário como data, sem hora (
DATEno banco,"2026-01-15"em JSON). - Nas APIs, troque instantes em ISO 8601 com o fuso explícito (
Zou-03:00). - Nos testes automatizados, rode com outro fuso (por exemplo,
TZ=Asia/Tokyo) para pegar código que depende do fuso da máquina.
Para converter entre data e timestamp Unix, veja o guia de timestamp. Para conferir a conta de anos, meses e dias entre duas datas do calendário, use a calculadora de diferença entre datas.
Achou um erro neste guia? Avise por e-mail.
Perguntas frequentes
Por que new Date("2026-01-15") mostra o dia 14?
Porque um texto só com a data é lido como meia-noite em UTC, e no Brasil esse instante ainda é o dia anterior, às 21h. Monte a data com new Date(2026, 0, 15) ou trate-a como texto se for uma data do calendário.
Como mostrar a data no horário de Brasília em JavaScript?
Use Intl.DateTimeFormat("pt-BR", { timeZone: "America/Sao_Paulo" }). Ele converte o instante para Brasília, qualquer que seja o fuso do computador ou do servidor.
O Brasil ainda tem horário de verão?
Não. O horário de verão foi extinto em 2019, e Brasília está em UTC-3 o ano todo desde então. Datas antigas, porém, ainda seguem as regras da época, e o banco de fusos do sistema cuida disso.
Devo salvar datas em UTC no banco?
Instantes, sim: salve em UTC e converta ao mostrar. Datas do calendário, como aniversários, devem ser salvas como data sem hora, porque não pertencem a nenhum fuso.
Outros guias
- Timestamp Unix: como converter data e hora em códigoEntenda o timestamp Unix, a diferença entre segundos e milissegundos e como converter de e para data em JavaScript, Python e PHP, com código testado.
- Expressão cron: como ler, escrever e acertar o fuso horárioOs cinco campos do cron, exemplos prontos, a regra do dia do mês com dia da semana e como converter o horário de Brasília para UTC, com código testado.