Apprenez à formater et convertir les dates en JavaScript sans surprises : timestamps, chaînes ISO, fuseaux horaires, DST, règles d'analyse et bonnes pratiques fiables.

Les bugs liés au temps en JavaScript n'apparaissent que rarement comme « l'horloge est fausse ». Ils se manifestent par de petits décalages confus : une date correcte sur votre portable mais erronée sur la machine d'un collègue, une réponse d'API qui semble juste jusqu'à ce qu'elle soit rendue dans un autre fuseau horaire, ou un rapport « déréglé » autour d'un changement de saison.
Vous remarquerez généralement un (ou plusieurs) de ces cas :
+02:00) n'est pas celui attendu.Une source majeure de problèmes est que le mot heure peut désigner des concepts différents :
2025-12-23T10:00:00Z). C'est généralement ce qu'on veut pour les logs, les événements et le stockage d'API.Le Date natif de JavaScript tente de couvrir tout cela, mais il représente principalement un instant dans le temps tout en vous poussant souvent vers un affichage local, ce qui facilite les conversions accidentelles.
Ce guide est volontairement pratique : comment obtenir des conversions prévisibles entre navigateurs et serveurs, comment choisir des formats plus sûrs (comme ISO 8601), et comment repérer les pièges classiques (secondes vs millisecondes, UTC vs local, et différences d'analyse). Le but n'est pas plus de théorie, mais moins de surprises « pourquoi ça a bougé ? ».
Les bugs de manipulation du temps en JavaScript commencent souvent par le mélange de représentations qui semblent interchangeables, mais ne le sont pas.
1) Millisecondes epoch (nombre)
Un simple nombre comme 1735689600000 est typiquement les « millisecondes depuis 1970-01-01T00:00:00Z ». Il représente un instant sans format ni fuseau horaire attaché.
2) Objet Date (wrapper autour d'un instant)
Un Date stocke le même type d'instant qu'un timestamp. La partie confuse : quand vous affichez un Date, JavaScript le formate en utilisant les règles locales de l'environnement, à moins que vous ne demandiez autre chose.
3) Chaîne formatée (affichage humain)
Des chaînes comme "2025-01-01", "01/01/2025 10:00", ou "2025-01-01T00:00:00Z" ne sont pas toutes équivalentes. Certaines sont univoques (ISO 8601 avec Z), d'autres dépendent du locale, et certaines n'incluent pas de fuseau horaire du tout.
Le même instant peut s'afficher différemment selon le fuseau horaire :
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" (jour précédent)
Choisissez une représentation interne unique (fréquemment millisecondes epoch ou ISO 8601 UTC) et respectez-la dans toute l'application et les API. Convertissez vers/depuis Date et les chaînes formatées uniquement aux frontières : parsing des entrées et affichage UI.
Un « timestamp » signifie généralement le temps epoch (aussi appelé Unix time) : le nombre d'unités écoulées depuis 1970-01-01 00:00:00 UTC. Le piège : différents systèmes utilisent des unités différentes.
Le Date de JavaScript est la source de la plupart des confusions parce qu'il utilise les millisecondes. De nombreuses API, bases de données et logs utilisent les secondes.
17040672001704067200000Même instant, mais la version en millisecondes a trois chiffres en plus.
Utilisez des multiplications/divisions explicites pour que l'unité soit évidente :
// 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()Cela paraît raisonnable, mais c'est faux lorsque ts est en secondes :
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
Le résultat sera une date en 1970, car 1,704,067,200 millisecondes ne représentent qu'environ 19 jours après l'époque.
Quand vous n'êtes pas sûr de l'unité, ajoutez des garde-fous rapides :
function asDateFromUnknownEpoch(x) {
// heuristic grossier : les secondes ~1e9-1e10, les millisecondes ~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());
Si le nombre de "chiffres" est ~10, c'est probablement des secondes. Si c'est ~13, c'est probablement des millisecondes. Imprimez aussi toISOString() pendant le débogage : c'est sans ambiguïté et vous repèrez tout de suite les erreurs d'unité.
Le Date de JavaScript peut être déroutant car il stocke un instant unique, mais peut présenter cet instant dans différents fuseaux horaires.
En interne, un Date est essentiellement « millisecondes depuis l'époque Unix (1970-01-01T00:00:00Z) ». Ce nombre représente un moment en UTC. Le "décalage" apparaît lorsque vous demandez à JavaScript de formater ce moment en heure locale (selon les paramètres de l'ordinateur/serveur) versus UTC.
Beaucoup d'API Date ont des variantes locales et UTC. Elles renvoient des valeurs différentes pour le même instant :
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // heure en *heure locale*
d.getUTCHours(); // heure en UTC
d.toString(); // chaîne en heure locale
d.toISOString(); // UTC (termine toujours par Z)
Si votre machine est à New York (UTC-5), ce temps UTC peut apparaître comme « 19:30 » la veille en local. Sur un serveur réglé en UTC, il apparaîtra comme « 00:30 ». Même instant, affichage différent.
Les logs utilisent souvent Date#toString() ou interpolent un Date de façon implicite, ce qui utilise le fuseau horaire local de l'environnement. Cela signifie que le même code peut afficher des timestamps différents sur votre portable, en CI et en production.
Stockez et transmettez le temps en UTC (ex. millisecondes epoch ou ISO 8601 avec Z). Convertissez en heure locale de l'utilisateur uniquement pour l'affichage :
toISOString() ou envoyez des millisecondes epochIntl.DateTimeFormatSi vous développez rapidement (par exemple avec un workflow type vibe-coding dans Koder.ai), il est utile d'intégrer cela tôt dans vos contrats d'API : nommez clairement les champs (createdAtMs, createdAtIso) et gardez le serveur (Go + PostgreSQL) et le client (React) cohérents sur ce que représente chaque champ.
Si vous devez envoyer des dates/heures entre un navigateur, un serveur et une base de données, les chaînes ISO 8601 sont le choix le plus sûr par défaut. Elles sont explicites, largement supportées et portent (surtout) l'information de fuseau horaire.
Deux bons formats d'échange :
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00Que signifie "Z" ?
Z signifie Zulu time, un autre nom pour UTC. Donc 2025-03-04T12:30:00Z est « 12:30 en UTC ».
Quand les offsets comme +02:00 sont importants ?
Les offsets sont cruciaux quand un événement est lié à un contexte de fuseau local (rendez-vous, réservations, heures d'ouverture). 2025-03-04T12:30:00+02:00 décrit un instant qui est deux heures en avance sur UTC, et ce n'est pas le même instant que 2025-03-04T12:30:00Z.
Des chaînes comme 03/04/2025 sont un piège : est-ce le 4 mars ou le 3 avril ? Différents utilisateurs et environnements l'interprètent différemment. Préférez 2025-03-04 (date ISO) ou un datetime ISO complet.
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
Ce comportement de "round-trip" est exactement ce qu'on veut pour les APIs : cohérent, prévisible et conscient du fuseau horaire.
Date.parse() semble pratique : on lui passe une chaîne et on obtient un timestamp. Le problème, c'est que pour tout ce qui n'est pas clairement ISO 8601, l'analyse peut dépendre d'heuristiques du navigateur. Ces heuristiques ont varié entre moteurs et versions, ce qui signifie qu'une même entrée peut être analysée différemment (ou pas du tout) selon l'endroit où votre code s'exécute.
Date.parse() peut varierJavaScript ne standardise l'analyse de façon fiable que pour les chaînes de type ISO 8601 (et encore, des détails comme le fuseau horaire importent). Pour des formats « conviviaux » — comme "03/04/2025", "March 4, 2025" ou "2025-3-4" — les navigateurs peuvent interpréter :
Si vous ne pouvez pas prédire la forme exacte de la chaîne, vous ne pouvez pas prédire le résultat.
YYYY-MM-DDUn piège courant est la forme date simple "YYYY-MM-DD" (par ex. "2025-01-15"). Beaucoup de développeurs s'attendent à ce qu'elle soit interprétée comme minuit local. En pratique, certains environnements traitent cette forme comme minuit UTC.
Cette différence compte : minuit UTC converti en heure locale peut devenir le jour précédent dans les zones à décalage négatif (ex. Amériques) ou décaler l'heure de façon inattendue. C'est une manière courante d'obtenir des bugs « pourquoi ma date est décalée d'un jour ? ».
Pour les entrées serveur/API :
2025-01-15T13:45:00Z ou 2025-01-15T13:45:00+02:00."YYYY-MM-DD") et n'utilisez pas Date sauf si vous définissez aussi le fuseau voulu.Pour la saisie utilisateur :
03/04/2025 à moins que votre UI n'impose le sens.Au lieu de compter sur Date.parse() qui « devine », choisissez l'une de ces approches :
new Date(year, monthIndex, day) pour les dates locales).Quand les données temporelles sont critiques, « ça parse sur ma machine » ne suffit pas — rendez vos règles d'analyse explicites et cohérentes.
Si votre objectif est « afficher une date/heure comme les gens s'attendent », le meilleur outil en JavaScript est Intl.DateTimeFormat. Il utilise les règles locales de l'utilisateur (ordre, séparateurs, noms de mois) et évite l'approche fragile d'assembler des chaînes à la main comme month + '/' + day.
Le formatage manuel code souvent en dur un style US, oublie des zéros non significatifs, ou produit des résultats déroutants 24/12 heures. Intl.DateTimeFormat rend aussi explicite dans quel fuseau horaire vous affichez — critique quand les données sont stockées en UTC mais l'UI doit refléter l'heure locale de l'utilisateur.
Pour « juste formater joliment », dateStyle et timeStyle sont les plus simples :
const d = new Date('2025-01-05T16:30:00Z');
// Locale de l'utilisateur + fuseau horaire local de l'utilisateur
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Forcer un fuseau horaire spécifique (utile pour les heures d'événements)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
Si vous avez besoin d'un cycle horaire cohérent (par ex. un réglage), utilisez hour12 :
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Choisissez une fonction de formatage par « type » de timestamp dans votre UI (heure d'un message, entrée de log, début d'événement) et gardez la décision timeZone intentionnelle :
Cela vous donne un affichage cohérent et adapté au locale sans maintenir un ensemble fragile de chaînes de format personnalisées.
L'heure d'été (DST) correspond aux moments où un fuseau horaire change son offset UTC (généralement d'une heure) à des dates spécifiques. La partie délicate : DST ne modifie pas seulement le décalage — elle modifie l'existence de certaines heures locales.
Quand les horloges avancent, une plage d'heures locales n'existe pas. Par exemple, dans de nombreuses régions, l'horloge saute de 01:59 à 03:00, donc 02:30 heure locale est "manquante".
Quand les horloges reculent, une plage d'heures locales se produit deux fois. Par exemple, 01:30 peut se produire une fois avant le changement et une autre après, ce qui signifie qu'une même heure locale peut désigner deux instants différents.
Ce n'est pas équivalent autour des frontières DST :
Si DST commence ce soir, "demain à 9:00" peut être à 23 heures d'écart. Si DST finit, cela peut être 25 heures d'écart.
// Scénario : programmer « même heure locale demain »
const d = new Date(2025, 2, 8, 9, 0); // 8 mars, 9:00 heure locale
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Autour de DST, plus24h et nextDaySameLocal peuvent différer d'une heure.
setHours peut vous surprendreSi vous faites quelque chose comme date.setHours(2, 30, 0, 0) un jour de « spring forward », JavaScript peut normaliser en une autre heure valide (souvent 03:30), parce que 02:30 n'existe pas en heure locale.
setDate) plutôt que d'ajouter des millisecondes.Z pour que l'instant soit sans ambiguïté.Une source courante de bugs est d'utiliser Date pour représenter quelque chose qui n'est pas un instant calendaire.
Un timestamp répond à « quand cela s'est produit ? » (un instant précis comme 2025-12-23T10:00:00Z). Une durée répond à « combien de temps ? » (comme « 3 minutes 12 secondes »). Ce sont des concepts différents, et les mélanger mène à des calculs confus et à des effets inattendus liés aux fuseaux/DST.
Date est le mauvais outil pour les duréesDate représente toujours un point sur la timeline relatif à une époque. Si vous stockez « 90 secondes » comme un Date, vous stockez en réalité "1970-01-01 plus 90 secondes" dans un fuseau particulier. Le formatage peut alors afficher 01:01:30, être décalé d'une heure, ou afficher une date que vous ne vouliez pas.
Pour les durées, préférez des nombres :
HH:mm:ssVoici un formateur simple qui fonctionne pour des compte-à-rebours et longueurs média :
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" (compte à rebours)
formatHMS(5423); // "01:30:23" (durée média)
Si vous convertissez depuis des minutes, multipliez d'abord (minutes * 60) et gardez la valeur numérique jusqu'au rendu.
Quand vous comparez des temps en JavaScript, l'approche la plus sûre est de comparer des nombres, pas des textes formatés. Un objet Date est essentiellement un wrapper autour d'un timestamp numérique (millisecondes epoch), donc vous voulez que les comparaisons se fassent comme "nombre vs nombre".
Utilisez getTime() (ou Date.valueOf(), qui renvoie le même nombre) pour comparer de manière fiable :
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a est avant
}
// Marche aussi :
if (+a < +b) {
// l'unaire + appelle valueOf()
}
Évitez de comparer des chaînes formatées comme "1/10/2025, 12:00 PM" — elles dépendent du locale et ne trieront pas correctement. L'exception principale est les chaînes ISO 8601 dans le même format et fuseau (ex. toutes en ...Z), qui sont triables lexicographiquement.
Le tri par temps est simple si vous triez par millisecondes epoch :
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Le filtrage d'items dans une plage suit la même idée :
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;
});
"Début de journée" dépend de si vous entendez heure locale ou UTC :
// Début/fin de journée locale
const d = new Date(2025, 0, 10); // 10 janv en heure locale
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);
// Début/fin de journée UTC
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));
Choisissez une définition tôt et tenez-vous-y pour toutes vos comparaisons et logique de plages.
Les bugs temporels semblent aléatoires jusqu'à ce que vous déterminiez ce que vous avez (timestamp ? chaîne ? Date ?) et où le décalage est introduit (parsing, conversion de fuseau, formatage).
Commencez par logger la même valeur de trois façons différentes. Cela révèle rapidement si le problème vient des secondes vs millisecondes, du local vs UTC, ou du parsing.
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());
Que rechercher :
toISOString() est complètement à côté (ex. année 1970 ou très futur), suspectez secondes vs millisecondes.toISOString() semble correct mais toString() est "décalé", vous voyez un problème d'affichage en fuseau local.getTimezoneOffset() change selon la date, vous traversez une heure d'été.Beaucoup de rapports « ça marche sur ma machine » tiennent simplement aux valeurs par défaut de l'environnement.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Si votre serveur tourne en UTC mais votre portable en zone locale, l'affichage formaté diffèrera à moins de spécifier explicitement timeZone.
Créez des tests unitaires autour des frontières DST et des heures critiques :
23:30 → 00:30Si vous itérez vite, envisagez d'intégrer ces tests dans votre scaffolding. Par exemple, quand vous générez une appli React + Go avec Koder.ai, ajoutez une petite suite de « time contract » en amont (exemples de payloads API + assertions parsing/formatting) pour attraper les régressions avant déploiement.
"2025-03-02 10:00".locale et (si nécessaire) timeZone.Une gestion fiable du temps en JavaScript consiste surtout à choisir une « source de vérité » et à être cohérent du stockage à l'affichage.
Stockez et calculez en UTC. Traitez l'heure locale comme un détail de présentation.
Transmettez les dates entre systèmes comme des chaînes ISO 8601 avec un offset explicite (de préférence Z). Si vous devez envoyer des epochs numériques, documentez l'unité et restez cohérent (les millisecondes sont le défaut courant en JS).
Formatez pour les humains avec Intl.DateTimeFormat (ou toLocaleString), et passez un timeZone explicite quand vous avez besoin d'un rendu déterministe (par ex. toujours afficher en UTC ou dans une région d'affaires spécifique).
Z (ex. 2025-12-23T10:15:00Z). Si vous utilisez des epochs, nommez le champ comme createdAtMs pour préciser l'unité.Envisagez une bibliothèque dédiée si vous avez besoin d'événements récurrents, de règles complexes de fuseau horaire, d'arithmétique sûre autour de DST ("même heure locale demain"), ou beaucoup de parsing d'entrées incohérentes. La valeur réside dans des APIs plus claires et moins de bugs aux bords.
Si vous voulez aller plus loin, consultez d'autres guides sur le temps à /blog. Si vous évaluez des outils ou options de support, voyez /pricing.