Pelajari cara memformat dan mengonversi waktu di JavaScript tanpa kejutan: timestamp, string ISO, zona waktu, DST, aturan parsing, dan pola yang dapat diandalkan.

Bug waktu di JavaScript jarang muncul sebagai “jam salah”. Mereka muncul sebagai pergeseran kecil yang membingungkan: tanggal yang benar di laptop Anda tapi salah di mesin rekan kerja, respons API yang tampak baik sampai dirender di zona waktu lain, atau laporan yang “melenceng satu” di sekitar perubahan musim.
Anda biasanya akan melihat satu (atau lebih) dari ini:
+02:00) berbeda dari yang Anda harapkan.Sumber sakit yang besar adalah kata waktu bisa merujuk ke konsep berbeda:
Date bawaan JavaScript mencoba mencakup semua ini, tetapi utamanya merepresentasikan instant dalam waktu sambil terus-menerus mendorong Anda ke arah tampilan lokal, sehingga konversi yang tidak disengaja mudah terjadi.
Panduan ini sengaja praktis: bagaimana mendapatkan konversi yang dapat diprediksi di browser dan server, cara memilih format yang lebih aman (seperti ISO 8601), dan bagaimana mengenali jebakan klasik (detik vs milidetik, UTC vs lokal, dan perbedaan parsing). Tujuannya bukan teori lebih banyak—melainkan lebih sedikit kejutan "kenapa bergeser?".
Bug waktu di JavaScript sering dimulai dengan mencampur representasi yang terlihat dapat saling ditukar, tetapi sebenarnya tidak.
1) Epoch milidetik (number)
Angka polos seperti 1735689600000 biasanya adalah "milidetik sejak 1970-01-01T00:00:00Z". Ini merepresentasikan sebuah instant dalam waktu tanpa format atau zona waktu yang terlampir.
2) Objek Date (pembungkus sebuah instant)
Date menyimpan jenis instant yang sama seperti timestamp. Bagian yang membingungkan: ketika Anda mencetak Date, JavaScript memformatnya menggunakan aturan lokal lingkungan Anda kecuali Anda meminta sebaliknya.
3) String terformat (tampilan manusia)
String seperti "2025-01-01", "01/01/2025 10:00", atau "2025-01-01T00:00:00Z" bukanlah satu hal yang tunggal. Beberapa tidak ambigu (ISO 8601 dengan Z), lainnya tergantung locale, dan beberapa tidak menyertakan zona waktu sama sekali.
Instant yang sama dapat ditampilkan berbeda berdasarkan zona waktu:
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" (hari sebelumnya)
Pilih satu representasi internal (umumnya epoch milidetik atau UTC ISO 8601) dan pertahankan di seluruh aplikasi dan API Anda. Konversi ke/dari Date dan string terformat hanya pada boundary: parsing input dan tampilan UI.
"Timestamp" biasanya berarti waktu epoch (juga disebut Unix time): jumlah waktu sejak 1970-01-01 00:00:00 UTC. Perangkapnya: sistem berbeda menghitung dalam unit yang berbeda.
Date JavaScript adalah sumber kebingungan karena menggunakan milidetik. Banyak API, database, dan log menggunakan detik.
17040672001704067200000Momen yang sama, tetapi versi milidetik punya tiga digit tambahan.
Gunakan perkalian/pembagian eksplisit supaya unit jelas:
// 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()Ini tampak masuk akal, tapi salah ketika ts dalam detik:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
Hasilnya akan menjadi tanggal di 1970, karena 1,704,067,200 milidetik hanya sekitar 19 hari setelah epoch.
Saat Anda tidak yakin unit apa yang Anda punya, tambahkan penjaga cepat:
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());
Jika jumlah "digit" sekitar 10, kemungkinan itu detik. Jika sekitar 13, kemungkinan milidetik. Juga cetak toISOString() saat debugging: itu tidak ambigu dan membantu Anda menemukan kesalahan unit seketika.
Date JavaScript bisa membingungkan karena menyimpan sebuah instant tunggal, tetapi bisa menampilkan instant itu dalam zona waktu berbeda.
Secara internal, Date pada dasarnya adalah "milidetik sejak epoch Unix (1970-01-01T00:00:00Z)". Angka itu merepresentasikan sebuah momen dalam UTC. Pergeseran terjadi ketika Anda meminta JavaScript memformat momen itu sebagai waktu lokal (berdasarkan pengaturan komputer/server) versus UTC.
Banyak API Date punya varian lokal dan UTC. Mereka mengembalikan angka berbeda untuk instant yang sama:
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)
Jika mesin Anda di New York (UTC-5), waktu UTC itu mungkin muncul sebagai "19:30" pada hari sebelumnya secara lokal. Pada server yang diatur ke UTC, ia akan muncul sebagai "00:30". Instant yang sama, tampilan berbeda.
Log sering menggunakan Date#toString() atau menginterpolasi Date secara implisit, yang menggunakan zona waktu lokal lingkungan. Itu berarti kode yang sama bisa mencetak timestamp berbeda di laptop Anda, CI, dan produksi.
Simpan dan kirim waktu sebagai UTC (mis. epoch milidetik atau ISO 8601 dengan Z). Konversi ke zona waktu pengguna hanya saat menampilkan:
toISOString() atau kirim epoch milidetikIntl.DateTimeFormatJika Anda membangun aplikasi dengan cepat (mis. alur kerja vibe-coding di Koder.ai), ada baiknya menetapkan ini ke dalam kontrak API yang dihasilkan sejak awal: beri nama field dengan jelas (createdAtMs, createdAtIso) dan jaga konsistensi server (Go + PostgreSQL) dan klien (React) terhadap apa yang setiap field wakili.
Jika Anda perlu mengirim tanggal/waktu antar browser, server, dan database, string ISO 8601 adalah default teraman. Mereka eksplisit, didukung luas, dan (yang terpenting) membawa informasi zona waktu.
Dua format pertukaran yang baik:
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00Apa arti "Z"?
Z berarti Zulu time, nama lain untuk UTC. Jadi 2025-03-04T12:30:00Z adalah "12:30 di UTC."
Kapan offset seperti +02:00 penting?
Offset krusial ketika sebuah event terkait konteks zona waktu lokal (janji, booking, jam buka toko). 2025-03-04T12:30:00+02:00 menggambarkan momen yang dua jam lebih cepat dari UTC, dan itu bukan instant yang sama dengan 2025-03-04T12:30:00Z.
String seperti 03/04/2025 adalah jebakan: apakah itu 4 Maret atau 3 April? Pengguna dan lingkungan berbeda menafsirkannya berbeda. Lebih baik 2025-03-04 (tanggal ISO) atau datetime ISO penuh.
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
Perilaku "round-trip" ini persis yang Anda inginkan untuk API: konsisten, dapat diprediksi, dan sadar zona waktu.
Date.parse() terasa praktis: beri dia string, dapatkan timestamp. Masalahnya, untuk apa pun yang bukan ISO 8601 jelas, parsing dapat bergantung pada heuristik browser. Heuristik itu berbeda antar engine dan versi, yang berarti input yang sama bisa diparse berbeda (atau tidak sama sekali) tergantung di mana kode dijalankan.
Date.parse() bisa bervariasiJavaScript hanya menstandarkan parsing secara andal untuk string bergaya ISO 8601 (dan bahkan di sana, detail seperti zona waktu berpengaruh). Untuk format "ramah"—seperti "03/04/2025", "March 4, 2025", atau "2025-3-4"—browser dapat menafsirkan:
Jika bentuk string tidak dapat Anda prediksi persis, Anda tidak dapat memprediksi hasilnya.
YYYY-MM-DDPerangkap umum adalah bentuk tanggal polos "YYYY-MM-DD" (mis. "2025-01-15"). Banyak pengembang mengharapkannya diinterpretasikan sebagai tengah malam lokal. Dalam praktiknya, beberapa lingkungan menganggap bentuk ini sebagai tengah malam UTC.
Perbedaan itu penting: tengah malam UTC yang dikonversi ke waktu lokal dapat menjadi hari sebelumnya di zona negatif (mis. Amerika) atau menggeser jam secara tak terduga. Ini cara mudah mendapatkan bug "kenapa tanggal saya melenceng satu hari?".
Untuk input server/API:
2025-01-15T13:45:00Z atau 2025-01-15T13:45:00+02:00."YYYY-MM-DD") dan hindari mengonversinya ke Date kecuali Anda juga mendefinisikan zona waktu yang dimaksud.Untuk input pengguna:
03/04/2025 kecuali UI Anda memaksa maknanya.Daripada mengandalkan Date.parse() untuk "menentukan sendiri", pilih salah satu pola ini:
new Date(year, monthIndex, day) untuk tanggal lokal).Ketika data waktu kritikal, "di mesin saya ter-parse" tidak cukup—buat aturan parsing Anda eksplisit dan konsisten.
Jika tujuan Anda adalah "menampilkan tanggal/waktu sebagaimana orang harapkan", alat terbaik di JavaScript adalah Intl.DateTimeFormat. Ia menggunakan aturan locale pengguna (urutan, pemisah, nama bulan) dan menghindari pendekatan rapuh merangkai string secara manual seperti month + '/' + day.
Pemformatan manual sering meng-hardcode keluaran gaya AS, lupa angka awal (leading zeros), atau menghasilkan hasil 24/12 jam yang membingungkan. Intl.DateTimeFormat juga membuat eksplisit zona waktu mana yang Anda tampilkan—kritis saat data disimpan di UTC tetapi UI harus mencerminkan waktu lokal pengguna.
Untuk "format yang rapi", dateStyle dan timeStyle adalah yang paling sederhana:
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));
Jika Anda perlu siklus jam yang konsisten (mis. toggle pengaturan), gunakan hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Pilih satu fungsi pemformatan per "jenis" timestamp di UI Anda (waktu pesan, entri log, mulai event), dan buat keputusan timeZone secara sengaja:
Ini memberi Anda keluaran yang konsisten dan ramah-locale tanpa memelihara sekumpulan string format kustom yang rapuh.
Daylight Saving Time (DST) adalah saat zona waktu mengubah offset UTC-nya (biasanya satu jam) pada tanggal tertentu. Bagian rumit: DST tidak hanya "mengubah offset"—ia mengubah eksistensi waktu lokal tertentu.
Saat jam maju (spring forward), rentang waktu lokal tidak pernah terjadi. Misalnya, di banyak wilayah, jam melompat dari 01:59 ke 03:00, jadi 02:30 waktu lokal adalah "hilang."
Saat jam mundur (fall back), rentang waktu lokal terjadi dua kali. Misalnya, 01:30 bisa terjadi sekali sebelum pergeseran dan sekali setelahnya, yang berarti waktu jam-dinding yang sama dapat merujuk ke dua instant berbeda.
Kedua ini tidak setara di sekitar batas DST:
Jika DST dimulai malam ini, "besok jam 9:00" mungkin hanya 23 jam jauhnya. Jika DST berakhir malam ini, mungkin 25 jam jauhnya.
// 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 bisa mengejutkan AndaJika Anda melakukan sesuatu seperti date.setHours(2, 30, 0, 0) pada hari "spring forward", JavaScript mungkin menormalkannya ke waktu valid lain (sering 03:30), karena 02:30 tidak ada dalam waktu lokal.
setDate) daripada menambahkan milidetik.Z sehingga instant tidak ambigu.Sumber bug umum adalah menggunakan Date untuk merepresentasikan sesuatu yang bukan momen kalender.
Sebuah timestamp menjawab "kapan ini terjadi?" (instant spesifik seperti 2025-12-23T10:00:00Z). Sebuah durasi menjawab "berapa lama?" (seperti "3 menit 12 detik"). Ini konsep berbeda, dan mencampurkannya menyebabkan matematika membingungkan dan efek zona waktu/DST yang tak terduga.
Date alat yang salah untuk durasiDate selalu merepresentasikan sebuah titik pada timeline relatif terhadap epoch. Jika Anda menyimpan "90 detik" sebagai Date, sebenarnya Anda menyimpan "1970-01-01 plus 90 detik" dalam zona waktu tertentu. Memformatnya bisa tiba-tiba menampilkan 01:01:30, bergeser satu jam, atau memilih tanggal yang tidak Anda maksud.
Untuk durasi, lebih baik gunakan angka polos:
HH:mm:ssBerikut formatter sederhana yang bekerja untuk countdown timer dan durasi media:
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)
Jika Anda mengonversi dari menit, kalikan dulu (minutes * 60) dan pertahankan nilai numerik sampai Anda merendernya.
Saat Anda membandingkan waktu di JavaScript, pendekatan paling aman adalah membandingkan angka, bukan teks yang diformat. Objek Date pada dasarnya adalah pembungkus di sekitar timestamp numerik (epoch milidetik), jadi Anda ingin perbandingan berakhir sebagai "angka vs angka".
Gunakan getTime() (atau Date.valueOf(), yang mengembalikan angka yang sama) untuk membandingkan secara andal:
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()
}
Hindari membandingkan string yang diformat seperti "1/10/2025, 12:00 PM"—itu bergantung locale dan tidak akan terurut dengan benar. Pengecualian utama adalah string ISO 8601 dalam format yang sama dan zona waktu yang sama (mis. semua ...Z), yang dapat diurutkan secara leksikografis.
Mengurutkan berdasarkan waktu sederhana jika Anda mengurutkan berdasarkan epoch milidetik:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Memfilter item dalam rentang sama idenya:
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;
});
"Awal hari" tergantung apakah yang Anda maksud waktu lokal atau 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));
Pilih satu definisi sejak awal dan pertahankan di seluruh logika perbandingan dan rentang Anda.
Bug waktu terasa acak sampai Anda menentukan apa yang Anda punya (timestamp? string? Date?) dan di mana pergeseran diperkenalkan (parsing, konversi zona waktu, pemformatan).
Mulailah dengan men-logging nilai yang sama dalam tiga cara berbeda. Ini cepat memperlihatkan apakah masalahnya detik vs milidetik, lokal vs UTC, atau parsing string.
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());
Apa yang dicari:
toISOString() sangat meleset (mis. tahun 1970 atau jauh di masa depan), curigai detik vs milidetik.toISOString() tampak benar tapi toString() "bergeser", Anda melihat isu tampilan zona waktu lokal.getTimezoneOffset() berubah tergantung tanggal, Anda melewati daylight saving time.Banyak laporan "berfungsi di mesin saya" hanyalah default lingkungan berbeda.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Jika server Anda berjalan di UTC tetapi laptop Anda di zona lokal, keluaran yang diformat akan berbeda kecuali Anda menentukan timeZone secara eksplisit.
Buat unit test di sekitar batas DST dan waktu "edge":
23:30 → 00:30Jika Anda cepat iterasi, pertimbangkan menjadikan test ini bagian dari scaffolding. Misalnya, ketika menghasilkan aplikasi React + Go di Koder.ai, Anda bisa menambahkan suite test "kontrak waktu" kecil sejak awal (contoh payload API + asersi parsing/pemformatan) sehingga regresi tertangkap sebelum deploy.
"2025-03-02 10:00".locale dan (jika perlu) timeZone.Penanganan waktu yang andal di JavaScript sebagian besar soal memilih "sumber kebenaran" dan konsisten dari penyimpanan hingga tampilan.
Simpan dan hitung di UTC. Perlakukan waktu lokal pengguna sebagai detail presentasi.
Kirim tanggal antar sistem sebagai string ISO 8601 dengan offset eksplisit (sebaiknya Z). Jika Anda harus mengirim epoch numerik, dokumentasikan unitnya dan pertahankan konsistensi (milidetik adalah default umum di JS).
Format untuk manusia dengan Intl.DateTimeFormat (atau toLocaleString), dan berikan timeZone eksplisit saat Anda membutuhkan keluaran deterministik (mis. selalu menampilkan waktu di UTC atau wilayah bisnis tertentu).
Z (mis. 2025-12-23T10:15:00Z). Jika memakai epoch, sertakan nama field seperti createdAtMs agar satuan jelas.Pertimbangkan library tanggal-waktu khusus jika Anda membutuhkan event rekuren, aturan zona waktu kompleks, aritmetika aman DST ("waktu lokal yang sama besok"), atau banyak parsing dari input tidak konsisten. Nilainya ada pada API yang lebih jelas dan lebih sedikit bug kasus tepi.
Jika Anda ingin mendalami, jelajahi panduan terkait waktu di /blog. Jika Anda sedang mengevaluasi tooling atau opsi dukungan, lihat /pricing.