# 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.

- Endereço oficial: https://pradev.com.br/guias/datas-e-fuso-horario-em-javascript
- Tipo: Guia
- Publicado em: 2026-10-01
- Atualizado em: 2026-10-01
- Autor: Leonardo Gaertner (https://pradev.com.br/sobre)
- Ferramentas: [Conversor de Timestamp Unix](https://pradev.com.br/timestamp-converter), [Diferença entre Datas](https://pradev.com.br/diferenca-entre-datas)

## 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](https://pradev.com.br/timestamp-converter).

## 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);
}
```

_JavaScript. 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
```

_Python_

```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);
}
```

_PHP_

## 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](https://pradev.com.br/guias/timestamp-unix-como-converter). Para conferir a conta de anos, meses e dias entre duas datas do calendário, use a calculadora de [diferença entre datas](https://pradev.com.br/diferenca-entre-datas).

## 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.

## Fontes

- [MDN: Date](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Date)
- [MDN (em inglês): Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)

## Guias relacionados

- [Timestamp Unix: como converter data e hora em código](https://pradev.com.br/guias/timestamp-unix-como-converter): Entenda 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ário](https://pradev.com.br/guias/expressao-cron-como-ler-e-escrever): Os 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.
