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.

Por Leonardo Gaertner · Publicado em

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.

JavaScript
// "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.

Python
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
<?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 timestamptz no PostgreSQL ou como timestamp Unix) e converta para o fuso só na hora de mostrar.
  • Guarde datas do calendário como data, sem hora (DATE no banco, "2026-01-15" em JSON).
  • Nas APIs, troque instantes em ISO 8601 com o fuso explícito (Z ou -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.