Impara a formattare e convertire il tempo in JavaScript senza sorprese: timestamp, stringhe ISO, fusi orari, ora legale, regole di parsing e pattern affidabili.

I bug legati al tempo in JavaScript raramente si manifestano come “l'orologio è sbagliato”. Si presentano come piccoli scostamenti puzzolenti: una data corretta sul tuo portatile ma sbagliata sul computer di un collega, una risposta API che sembra giusta finché non viene renderizzata in un altro fuso orario, o un report che è “sbagliato di un giorno” intorno a un cambio stagionale.
Di solito noterai uno (o più) di questi:
+02:00) è diverso da quanto ci si aspettava.Una grande fonte di dolore è che la parola tempo può riferirsi a concetti diversi:
Il Date incorporato in JavaScript cerca di coprire tutti questi casi, ma rappresenta principalmente un istante nel tempo mentre ti spinge continuamente verso la visualizzazione locale, il che rende facili le conversioni accidentali.
Questa guida è intenzionalmente pratica: come ottenere conversioni prevedibili tra browser e server, come scegliere formati più sicuri (come ISO 8601) e come individuare le trappole classiche (secondi vs millisecondi, UTC vs locale e differenze di parsing). L'obiettivo non è più teoria, ma meno sorprese del tipo “perché si è spostato?”.
I bug temporali in JavaScript spesso iniziano mescolando rappresentazioni che sembrano intercambiabili, ma non lo sono.
1) Epoch milliseconds (numero)
Un numero semplice come 1735689600000 è tipicamente “millisecondi dal 1970-01-01T00:00:00Z”. Rappresenta un istante nel tempo senza formattazione o fuso orario.
2) Oggetto Date (wrapper attorno a un istante)
Un Date memorizza lo stesso tipo di istante di un timestamp. La parte confusa: quando stampi un Date, JavaScript lo formatta usando le regole locali dell'ambiente a meno che non chiedi diversamente.
3) Stringa formattata (visualizzazione umana)
Stringhe come "2025-01-01", "01/01/2025 10:00", o "2025-01-01T00:00:00Z" non sono tutte la stessa cosa. Alcune sono inequivocabili (ISO 8601 con Z), altre dipendono dalla locale, e alcune non includono affatto il fuso orario.
Lo stesso istante può essere mostrato diversamente a seconda del fuso orario:
const instant = new Date("2025-01-01T00:00:00Z");
instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"
instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (giorno precedente)
Scegli una sola rappresentazione interna (comunemente epoch milliseconds o ISO 8601 UTC) e mantienila in tutta l'app e nelle API. Converti verso/da Date e stringhe formattate solo ai confini: parsing dell'input e visualizzazione in UI.
Un “timestamp” solitamente significa tempo epoch (detto anche Unix time): il conteggio del tempo a partire da 1970-01-01 00:00:00 UTC. Il problema: sistemi diversi usano unità diverse.
Il Date di JavaScript è la fonte della maggior parte della confusione perché usa millisecondi. Molte API, database e log usano secondi.
17040672001704067200000Stesso momento, ma la versione in millisecondi ha tre cifre in più.
Usa moltiplicazioni/divisioni esplicite così l'unità è ovvia:
// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);
// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);
// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);
// Date -> milliseconds
const msOut = d2.getTime();
Date()Questo sembra ragionevole, ma è sbagliato quando ts è in secondi:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
Il risultato sarà una data nel 1970, perché 1,704,067,200 millisecondi sono solo circa 19 giorni dopo l'epoch.
Quando non sei sicuro dell'unità che hai, aggiungi guardrail rapidi:
function asDateFromUnknownEpoch(x) {
// crude heuristic: seconds are ~1e9-1e10, milliseconds are ~1e12-1e13
if (x < 1e11) return new Date(x * 1000); // assume seconds
return new Date(x); // assume milliseconds
}
const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());
Se il conteggio delle “cifre” è ~10 probabilmente sono secondi. Se è ~13 probabilmente sono millisecondi. Stampare toISOString() durante il debug aiuta a individuare immediatamente errori di unità.
Il Date di JavaScript può essere confuso perché memorizza un singolo istante nel tempo, ma può presentare quell'istante in fusi orari diversi.
Internamente, un Date è essenzialmente “millisecondi dall'epoch Unix (1970-01-01T00:00:00Z)”. Quel numero rappresenta un momento in UTC. Lo “spostamento” avviene quando chiedi a JavaScript di formattare quell'istante come ora locale (basata sulle impostazioni del computer/server) invece che in UTC.
Molte API di Date hanno varianti locali e UTC. Restituiscono numeri diversi per lo stesso istante:
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // hour in *local* time zone
d.getUTCHours(); // hour in UTC
d.toString(); // local time string
d.toISOString(); // UTC (always ends with Z)
Se la tua macchina è a New York (UTC-5), quell'ora UTC può apparire come “19:30” del giorno precedente in locale. Su un server impostato su UTC apparirà come “00:30”. Stesso istante, visualizzazioni diverse.
I log spesso usano Date#toString() o interpolano un Date implicitamente, che utilizza il fuso orario locale dell'ambiente. Questo significa che lo stesso codice può stampare timestamp diversi sul tuo laptop, in CI e in produzione.
Memorizza e trasmetti il tempo in UTC (es. epoch milliseconds o ISO 8601 con Z). Convertilo nella locale dell'utente solo quando mostri i dati:
toISOString() o invia epoch millisecondsIntl.DateTimeFormatSe costruisci un'app rapidamente, aiuta definire questo nei contratti API generati in anticipo: nomina chiaramente i campi (createdAtMs, createdAtIso) e mantieni server (Go + PostgreSQL) e client (React) coerenti su cosa rappresenta ogni campo.
Se devi inviare date/tempi tra browser, server e database, le stringhe ISO 8601 sono il default più sicuro. Sono esplicite, ampiamente supportate e (soprattutto) contengono informazioni sul fuso orario.
Due buoni formati di scambio:
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00Cosa significa “Z”?
Z sta per Zulu time, un altro nome per UTC. Quindi 2025-03-04T12:30:00Z è “12:30 in UTC”.
Quando gli offset come +02:00 sono importanti?
Gli offset sono cruciali quando un evento è legato a un contesto di fuso locale (appuntamenti, prenotazioni, orari di apertura). 2025-03-04T12:30:00+02:00 descrive un istante che è due ore avanti rispetto a UTC, e non è lo stesso istante di 2025-03-04T12:30:00Z.
Stringhe come 03/04/2025 sono una trappola: è il 4 marzo o il 3 aprile? Diversi utenti ed ambienti lo interpretano in modo diverso. Preferisci 2025-03-04 (data ISO) o un datetime ISO completo.
const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();
console.log(iso); // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z
Quel comportamento di “round-trip” è esattamente ciò che vuoi per le API: consistente, prevedibile e consapevole del fuso orario.
Date.parse() sembra comodo: gli passi una stringa e ottieni un timestamp. Il problema è che per tutto ciò che non è chiaramente ISO 8601, il parsing può dipendere da euristiche del browser. Quelle euristiche sono cambiate tra motori e versioni, il che significa che lo stesso input può essere interpretato diversamente (o non interpretato) a seconda di dove gira il codice.
Date.parse() può variareJavaScript standardizza il parsing in modo affidabile solo per stringhe stile ISO 8601 (e anche lì dettagli come il fuso orario contano). Per formati “friendly” — come "03/04/2025", "March 4, 2025" o "2025-3-4" — i browser possono interpretare:
Se non puoi prevedere la forma esatta della stringa, non puoi prevedere il risultato.
YYYY-MM-DDUna trappola comune è la forma "YYYY-MM-DD" (per esempio, "2025-01-15"). Molti sviluppatori si aspettano che venga interpretata come mezzanotte locale. In pratica, alcuni ambienti trattano questa forma come mezzanotte UTC.
Questa differenza conta: la mezzanotte UTC convertita in ora locale può diventare il giorno precedente in zone negative (es. Americhe) o spostare l'ora inaspettatamente. È un modo facile per ottenere bug “perché la mia data è sbagliata di un giorno?”.
Per input server/API:
2025-01-15T13:45:00Z o 2025-01-15T13:45:00+02:00."YYYY-MM-DD") ed evita di convertirlo in Date a meno che non definisci anche il fuso orario intenzionato.Per input utente:
03/04/2025 a meno che la UI non imponga il significato.Invece di affidarti a Date.parse() per “capirlo”, scegli uno di questi approcci:
new Date(year, monthIndex, day) per date locali).Quando i dati temporali sono critici, “parse funziona sulla mia macchina” non basta — rendi le regole di parsing esplicite e coerenti.
Se il tuo obiettivo è “mostrare una data/ora nel modo che le persone si aspettano”, lo strumento migliore in JavaScript è Intl.DateTimeFormat. Usa le regole di locale dell'utente (ordine, separatori, nomi dei mesi) ed evita l'approccio fragile di costruire manualmente stringhe come month + '/' + day.
La formattazione manuale spesso fissa l'output in stile USA, dimentica gli zeri iniziali o produce risultati 24/12 ore confusi. Intl.DateTimeFormat rende anche esplicito quale fuso orario stai mostrando—critico quando i dati sono memorizzati in UTC ma la UI deve riflettere l'ora locale dell'utente.
Per “formatta semplicemente”, dateStyle e timeStyle sono i più semplici:
const d = new Date('2025-01-05T16:30:00Z');
// User’s locale + user’s local time zone
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Force a specific time zone (great for event times)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
Se hai bisogno di un ciclo orario consistente (es. un toggle nelle impostazioni), usa hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Scegli una funzione di formattazione per ogni “tipo” di timestamp nella UI (ora del messaggio, voce di log, inizio evento) e mantieni la decisione sul timeZone intenzionale:
Questo ti dà un output coerente e amichevole per la locale senza dover mantenere una serie fragile di stringhe di formato personalizzate.
L'ora legale (DST) è quando un fuso orario cambia il suo offset UTC (tipicamente di un'ora) in date specifiche. La parte difficoltosa: il DST non cambia solo l'offset—cambia l'esistenza di certe ore locali.
Quando l'orologio avanza (spring forward), un intervallo di ore locali non esiste. Per esempio, in molte regioni l'orologio salta da 01:59 a 03:00, quindi le 02:30 locali sono “invisibili”.
Quando l'orologio torna indietro (fall back), un intervallo di ore locali accade due volte. Per esempio, 01:30 può verificarsi una volta prima del cambio e una dopo, il che significa che la stessa ora sul quadrante può riferirsi a due istanti diversi.
Queste due cose non sono equivalenti attorno ai confini DST:
Se stanotte entra in vigore l'ora legale, “domani alle 9:00” potrebbe essere a 23 ore di distanza. Se stanotte l'ora termina, potrebbe essere a 25 ore di distanza.
// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.
setHours può sorprendertiSe fai qualcosa come date.setHours(2, 30, 0, 0) in un giorno di “spring forward”, JavaScript può normalizzarlo a un'ora valida diversa (spesso 03:30), perché le 02:30 non esistono in locale.
setDate) invece di aggiungere millisecondi.Z così l'istante è inequivocabile.Una fonte comune di bug è usare Date per rappresentare qualcosa che non è un momento sul calendario.
Un timestamp risponde a “quando è successo?” (un istante specifico come 2025-12-23T10:00:00Z). Una durata risponde a “per quanto tempo?” (come “3 minuti 12 secondi”). Sono concetti diversi, e mescolarli porta a calcoli confusi ed effetti indesiderati legati a fuso orario/DST.
Date è lo strumento sbagliato per durateDate rappresenta sempre un punto sulla linea temporale relativo a un epoch. Se memorizzi “90 secondi” come Date, stai davvero memorizzando “1970-01-01 più 90 secondi” in un fuso orario. Formattandolo potresti vedere 01:01:30, subire uno spostamento di un'ora o mostrare una data che non volevi.
Per le durate, preferisci numeri semplici:
HH:mm:ssEcco un semplice formatter che funziona per timer e lunghezze multimediali:
function formatHMS(totalSeconds) {
const s = Math.max(0, Math.floor(totalSeconds));
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
formatHMS(75); // "00:01:15" (countdown timer)
formatHMS(5423); // "01:30:23" (media duration)
Se devi convertire da minuti, moltiplica prima (minutes * 60) e mantieni il valore numerico fino al rendering.
Quando confronti orari in JavaScript, l'approccio più sicuro è confrontare numeri, non testo formattato. Un oggetto Date è essenzialmente un wrapper attorno a un timestamp numerico (epoch milliseconds), quindi vuoi che i confronti finiscano come “numero vs numero”.
Usa getTime() (o Date.valueOf(), che restituisce lo stesso numero) per confrontare in modo affidabile:
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a is earlier
}
// Also works:
if (+a < +b) {
// unary + calls valueOf()
}
Evita di confrontare stringhe formattate come "1/10/2025, 12:00 PM"—quelle dipendono dalla locale e non si ordineranno correttamente. L'eccezione principale sono le stringhe ISO 8601 nello stesso formato e fuso orario (es. tutte ...Z), che sono ordinabili lessicograficamente.
Ordinare per tempo è semplice se ordini per epoch milliseconds:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Filtrare elementi entro un intervallo è la stessa idea:
const start = new Date('2025-01-01T00:00:00Z').getTime();
const end = new Date('2025-02-01T00:00:00Z').getTime();
const inRange = items.filter(i => {
const t = new Date(i.createdAt).getTime();
return t >= start && t < end;
});
“Start of day” dipende se intendi ora locale o UTC:
// Local start/end of day
const d = new Date(2025, 0, 10); // Jan 10 in local time
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
// UTC start/end of day
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));
Scegli una definizione presto e mantienila nelle tue logiche di confronto e range.
I bug temporali sembrano casuali finché non individui cosa hai (timestamp? stringa? Date?) e dove si introduce lo scostamento (parsing, conversione di fuso, formattazione).
Inizia loggando lo stesso valore in tre modi diversi. Questo rivela velocemente se il problema è secondi vs millisecondi, locale vs UTC o parsing di stringhe.
console.log('raw input:', input);
const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());
Cosa cercare:
toISOString() è completamente sbagliato (es. anno 1970 o un futuro lontano), sospetta secondi vs millisecondi.toISOString() sembra giusto ma toString() è “spostato”, stai vedendo un problema di visualizzazione nel fuso locale.getTimezoneOffset() cambia a seconda della data, stai attraversando l'ora legale.Molti “funziona sulla mia macchina” sono semplicemente default ambientali diversi.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Se il server gira in UTC ma il tuo portatile in un fuso locale, l'output formattato differirà a meno che non specifichi esplicitamente un timeZone.
Crea test unitari attorno ai boundary DST e agli orari “edge”:
23:30 → 00:30Se iteri velocemente, considera di rendere questi test parte del tuo scaffolding. Per esempio, quando generi una app React + Go con Koder.ai, puoi aggiungere una piccola suite di “time contract” all'inizio (esempi di payload API + asserzioni di parsing/formattazione) così le regressioni vengono intercettate prima del deploy.
"2025-03-02 10:00".locale e (quando serve) timeZone.La gestione affidabile del tempo in JavaScript è per lo più scegliere una “fonte di verità” e mantenere coerenza da storage a display.
Memorizza e calcola in UTC. Tratta l'ora locale dell'utente come un dettaglio di presentazione.
Trasmetti date tra sistemi come stringhe ISO 8601 con offset esplicito (preferibilmente Z). Se devi inviare epoch numerici, documenta l'unità e mantienila coerente (millisecondi è il default comune in JS).
Formatta per gli utenti con Intl.DateTimeFormat (o toLocaleString) e passa un timeZone esplicito quando vuoi output deterministico (per esempio, mostrare sempre orari in UTC o in una regione business specifica).
Z (es. 2025-12-23T10:15:00Z). Se usi epoch, includi un nome campo come createdAtMs per chiarire le unità.Considera una libreria dedicata se ti servono eventi ricorrenti, regole complesse sui fusi orari, aritmetica sicura rispetto a DST (“stessa ora locale domani”) o molto parsing da input incoerenti. Il valore sta nelle API più chiare e in meno bug di edge-case.
Se vuoi approfondire, consulta le guide nel blog. Se stai valutando strumenti o opzioni di supporto, consulta la pagina dei prezzi.