Timers #

Timer adalah mekanisme untuk menjadwalkan eksekusi kode di masa depan — baik sekali setelah delay tertentu, berulang secara periodik, atau di iterasi event loop berikutnya. Di Node.js, timer bukan bagian dari JavaScript engine itu sendiri, melainkan disediakan oleh libuv melalui event loop. Memahami bagaimana timer berinteraksi dengan event loop bukan sekadar pengetahuan akademis — ini langsung mempengaruhi urutan eksekusi kode asinkron, performa aplikasi, dan kebenaran logika yang bergantung pada waktu.

Event Loop dan Timer #

Sebelum masuk ke API, penting memahami di mana timer berada dalam siklus event loop Node.js.

flowchart TD
    A[Event Loop mulai] --> B["timers\nsetTimeout, setInterval\nyang sudah jatuh tempo"]
    B --> C["pending callbacks\nI/O callbacks dari iterasi sebelumnya"]
    C --> D["idle, prepare\ninternal Node.js"]
    D --> E["poll\ntunggu I/O baru"]
    E --> F["check\nsetImmediate callbacks"]
    F --> G["close callbacks\nsocket.on('close', ...)"]
    G --> A

    H["Microtask Queue\nPromise.then, queueMicrotask"] -- "dijalankan setelah\nsetiap phase" --> B

Urutan prioritas (dari paling cepat dieksekusi):

1. Microtask queue    — Promise.then(), queueMicrotask()
2. timers phase       — setTimeout(fn, 0), setInterval(fn, 0)
3. check phase        — setImmediate()
// demonstrasi urutan eksekusi
console.log("1 — sync");

setTimeout(() => console.log("4 — setTimeout 0"), 0);

setImmediate(() => console.log("5 — setImmediate"));

Promise.resolve().then(() => console.log("2 — Promise.then"));

queueMicrotask(() => console.log("3 — queueMicrotask"));

console.log("6 — sync setelah setup");

// Output:
// 1 — sync
// 6 — sync setelah setup
// 2 — Promise.then
// 3 — queueMicrotask
// 4 — setTimeout 0
// 5 — setImmediate

setTimeout — Eksekusi Sekali Setelah Delay #

setTimeout menjadwalkan fungsi untuk dijalankan sekali setelah delay minimum dalam milidetik. “Minimum” adalah kata kunci — delay aktual bisa lebih lama jika event loop sedang sibuk.

// setTimeout dasar
const timerId = setTimeout(() => {
  console.log("Dijalankan setelah 1 detik");
}, 1000);

// batalkan sebelum dijalankan
clearTimeout(timerId);

// setTimeout dengan argumen — argumen diteruskan ke callback
setTimeout((nama: string, angka: number) => {
  console.log(`Halo ${nama}, angka: ${angka}`);
}, 500, "Budi", 42);

// delay 0 — jadwalkan di iterasi event loop berikutnya
setTimeout(() => {
  console.log("Ini berjalan setelah kode sync saat ini selesai");
}, 0);

// promise-based setTimeout — lebih bersih dengan async/await
function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// delay yang bisa dibatalkan dengan AbortController
function delayDenganAbort(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(new DOMException("Dibatalkan", "AbortError"));
      return;
    }

    const timerId = setTimeout(resolve, ms);

    signal?.addEventListener("abort", () => {
      clearTimeout(timerId);
      reject(new DOMException("Dibatalkan", "AbortError"));
    }, { once: true });
  });
}

// penggunaan
async function contohDelay(): Promise<void> {
  console.log("Mulai");
  await delay(1000);
  console.log("Setelah 1 detik");

  const controller = new AbortController();
  setTimeout(() => controller.abort(), 500); // batalkan setelah 0.5 detik

  try {
    await delayDenganAbort(2000, controller.signal);
    console.log("Ini tidak akan dicetak");
  } catch (err) {
    if (err instanceof DOMException && err.name === "AbortError") {
      console.log("Delay dibatalkan");
    }
  }
}

setTimeout untuk Retry Logic #

async function retryDenganBackoff<T>(
  fn: () => Promise<T>,
  options: {
    maksRetry?: number;
    delayAwal?: number;
    faktorBackoff?: number;
    delayMaks?: number;
    shouldRetry?: (err: unknown) => boolean;
  } = {}
): Promise<T> {
  const {
    maksRetry = 3,
    delayAwal = 1000,
    faktorBackoff = 2,
    delayMaks = 30_000,
    shouldRetry = () => true,
  } = options;

  let percobaan = 0;
  let delayMs = delayAwal;

  while (true) {
    try {
      return await fn();
    } catch (err) {
      percobaan++;

      if (percobaan >= maksRetry || !shouldRetry(err)) {
        throw err;
      }

      console.log(`Percobaan ${percobaan} gagal, retry dalam ${delayMs}ms...`);

      await delay(delayMs);

      // exponential backoff dengan jitter
      delayMs = Math.min(
        delayMs * faktorBackoff + Math.random() * 1000,
        delayMaks
      );
    }
  }
}

// penggunaan
const data = await retryDenganBackoff(
  () => fetch("https://api.example.com/data").then((r) => r.json()),
  {
    maksRetry: 5,
    delayAwal: 500,
    faktorBackoff: 2,
    shouldRetry: (err) => {
      // hanya retry untuk error jaringan, bukan 4xx
      return !(err instanceof Response && err.status >= 400 && err.status < 500);
    },
  }
);

setInterval — Eksekusi Berulang #

setInterval menjalankan callback secara periodik. Perhatikan bahwa interval sebenarnya adalah waktu antara akhir eksekusi sebelumnya dan mulai eksekusi berikutnya hanya jika callback-nya sinkron — untuk callback asinkron yang butuh waktu lebih lama dari interval, eksekusi bisa menumpuk.

// setInterval dasar
const intervalId = setInterval(() => {
  console.log("Dijalankan setiap detik:", new Date().toISOString());
}, 1000);

// batalkan setelah 5 kali
let hitungan = 0;
const id = setInterval(() => {
  hitungan++;
  console.log(`Iterasi ke-${hitungan}`);
  if (hitungan >= 5) clearInterval(id);
}, 200);

// ANTI-PATTERN: setInterval dengan callback asinkron yang mungkin overlap
setInterval(async () => {
  await operasiLambat(); // ✗ jika butuh > interval, eksekusi menumpuk
}, 1000);

// BENAR: gunakan setTimeout rekursif untuk memastikan tidak overlap
async function jalankanBerulang(
  fn: () => Promise<void>,
  intervalMs: number
): Promise<() => void> {
  let berjalan = true;

  async function loop(): Promise<void> {
    while (berjalan) {
      const mulai = Date.now();
      try {
        await fn();
      } catch (err) {
        console.error("Error dalam loop berulang:", err);
      }
      const durasi = Date.now() - mulai;
      const sisaDelay = Math.max(0, intervalMs - durasi);
      // tunggu sisa interval setelah fn selesai
      if (berjalan) await delay(sisaDelay);
    }
  }

  loop(); // mulai loop tanpa await

  // kembalikan fungsi untuk menghentikan loop
  return () => { berjalan = false; };
}

async function operasiLambat(): Promise<void> {
  await delay(500);
  console.log("Operasi selesai");
}

const hentikan = await jalankanBerulang(operasiLambat, 2000);
// setelah beberapa saat...
setTimeout(hentikan, 10_000); // hentikan setelah 10 detik

Polling dengan setInterval #

// polling status job yang sedang berjalan
async function polling<T>(
  cek: () => Promise<T | null>,
  options: {
    intervalMs?: number;
    timeoutMs?: number;
    pesan?: string;
  } = {}
): Promise<T> {
  const { intervalMs = 2000, timeoutMs = 60_000, pesan = "Menunggu..." } = options;

  const mulai = Date.now();

  while (true) {
    const hasil = await cek();

    if (hasil !== null) {
      return hasil;
    }

    if (Date.now() - mulai > timeoutMs) {
      throw new Error(`Polling timeout setelah ${timeoutMs}ms`);
    }

    console.log(pesan);
    await delay(intervalMs);
  }
}

// contoh: polling status export file
async function tunggExport(jobId: string): Promise<string> {
  const url = await polling(
    async () => {
      const status = await cekStatusJob(jobId);
      if (status.selesai) return status.downloadUrl;
      return null;
    },
    {
      intervalMs: 3000,
      timeoutMs: 5 * 60_000, // 5 menit
      pesan: `Menunggu export job ${jobId}...`,
    }
  );
  return url;
}

async function cekStatusJob(_jobId: string): Promise<{ selesai: boolean; downloadUrl: string }> {
  // implementasi cek status job
  return { selesai: false, downloadUrl: "" };
}

setImmediate — Eksekusi di Akhir Iterasi Saat Ini #

setImmediate menjadwalkan callback untuk dijalankan di phase check — setelah I/O events tapi sebelum timer berikutnya. Berguna untuk memecah operasi CPU-intensive agar tidak memblokir event loop.

// setImmediate — dijalankan di akhir iterasi event loop saat ini
setImmediate(() => {
  console.log("setImmediate dipanggil");
});

// membatalkan
const immediateId = setImmediate(() => {
  console.log("Ini tidak akan berjalan");
});
clearImmediate(immediateId);

// gunakan setImmediate untuk operasi CPU-intensive yang panjang
// agar tidak memblokir I/O di tengah-tengah proses
async function prosesDataBesar(data: number[]): Promise<number> {
  let total = 0;
  const chunkSize = 10_000;

  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);

    // proses satu chunk
    for (const angka of chunk) {
      total += angka;
    }

    // yield ke event loop setelah setiap chunk
    // memungkinkan I/O dan event lain diproses di antara chunk
    if (i + chunkSize < data.length) {
      await new Promise<void>((resolve) => setImmediate(resolve));
    }
  }

  return total;
}

// contoh: proses 1 juta angka tanpa memblokir server
const data = Array.from({ length: 1_000_000 }, (_, i) => i + 1);
const total = await prosesDataBesar(data);
console.log("Total:", total); // 500000500000

queueMicrotask — Microtask Queue #

queueMicrotask menambahkan callback ke microtask queue — dieksekusi sebelum timer apapun, bahkan sebelum setImmediate. Sama dengan Promise.resolve().then() tapi tanpa overhead membuat Promise.

// queueMicrotask — paling cepat, sebelum timer
queueMicrotask(() => {
  console.log("microtask");
});

setTimeout(() => console.log("setTimeout"), 0);

// Output:
// microtask
// setTimeout

// use case: batch update — kumpulkan beberapa perubahan dan proses sekaligus
class BatchUpdater {
  private perubahan = new Set<string>();
  private terjadwal = false;

  tandaiPerubahan(key: string): void {
    this.perubahan.add(key);

    // jadwalkan flush di microtask berikutnya
    // jika dipanggil berkali-kali secara sinkron, flush hanya terjadi sekali
    if (!this.terjadwal) {
      this.terjadwal = true;
      queueMicrotask(() => this.flush());
    }
  }

  private flush(): void {
    console.log("Memproses perubahan:", [...this.perubahan]);
    this.perubahan.clear();
    this.terjadwal = false;
  }
}

const updater = new BatchUpdater();

// semua perubahan ini dikumpulkan dan diproses sekali
updater.tandaiPerubahan("user.nama");
updater.tandaiPerubahan("user.email");
updater.tandaiPerubahan("user.alamat");
// Output: Memproses perubahan: ["user.nama", "user.email", "user.alamat"]

Debounce dan Throttle #

Debounce dan throttle adalah pola untuk membatasi seberapa sering sebuah fungsi dijalankan — sangat umum untuk event UI seperti input, scroll, dan resize.

flowchart LR
    subgraph "Tanpa debounce/throttle"
        A1[event] --> F1[fn]
        A2[event] --> F2[fn]
        A3[event] --> F3[fn]
        A4[event] --> F4[fn]
        A5[event] --> F5[fn]
    end

    subgraph "Dengan debounce (300ms)"
        B1[event]
        B2[event]
        B3[event]
        B4[event]
        B5[event] --> G1[fn — sekali setelah berhenti]
    end

    subgraph "Dengan throttle (300ms)"
        C1[event] --> H1[fn]
        C2[event]
        C3[event] --> H2[fn]
        C4[event]
        C5[event] --> H3[fn]
    end

Debounce #

Debounce menunda eksekusi sampai tidak ada panggilan baru selama waktu tertentu. Cocok untuk search-as-you-type, validasi form, atau auto-save.

function debounce<T extends (...args: unknown[]) => unknown>(
  fn: T,
  delayMs: number
): {
  (...args: Parameters<T>): void;
  cancel: () => void;
  flush: (...args: Parameters<T>) => void;
} {
  let timerId: ReturnType<typeof setTimeout> | null = null;

  function debounced(...args: Parameters<T>): void {
    // batalkan timer sebelumnya setiap kali dipanggil
    if (timerId !== null) clearTimeout(timerId);

    timerId = setTimeout(() => {
      timerId = null;
      fn(...args);
    }, delayMs);
  }

  // batalkan tanpa menjalankan
  debounced.cancel = (): void => {
    if (timerId !== null) {
      clearTimeout(timerId);
      timerId = null;
    }
  };

  // jalankan segera dan batalkan timer yang pending
  debounced.flush = (...args: Parameters<T>): void => {
    debounced.cancel();
    fn(...args);
  };

  return debounced;
}

// contoh: pencarian dengan debounce
const cariProduk = debounce(async (keyword: string) => {
  console.log("Mencari:", keyword);
  // await fetch(`/api/produk?q=${keyword}`)
}, 300);

// saat user mengetik "laptop gaming" karakter per karakter
// hanya satu request yang dikirim — 300ms setelah berhenti mengetik
cariProduk("l");
cariProduk("la");
cariProduk("lap");
cariProduk("lapt");
cariProduk("lapto");
cariProduk("laptop");
cariProduk("laptop ");
cariProduk("laptop g");
cariProduk("laptop ga");
cariProduk("laptop gam");
cariProduk("laptop gami");
cariProduk("laptop gamin");
cariProduk("laptop gaming");
// Setelah 300ms tidak ada input lagi:
// Output: Mencari: laptop gaming

Throttle #

Throttle memastikan fungsi dijalankan paling banyak sekali dalam interval waktu tertentu. Cocok untuk scroll handler, resize handler, atau rate limiting API call dari sisi client.

function throttle<T extends (...args: unknown[]) => unknown>(
  fn: T,
  intervalMs: number
): (...args: Parameters<T>) => void {
  let terakhirDijalankan = 0;
  let timerId: ReturnType<typeof setTimeout> | null = null;

  return function throttled(...args: Parameters<T>): void {
    const sekarang = Date.now();
    const sisaWaktu = intervalMs - (sekarang - terakhirDijalankan);

    if (sisaWaktu <= 0) {
      // sudah lewat interval — jalankan sekarang
      if (timerId !== null) {
        clearTimeout(timerId);
        timerId = null;
      }
      terakhirDijalankan = sekarang;
      fn(...args);
    } else {
      // belum lewat interval — jadwalkan di akhir interval
      if (timerId !== null) clearTimeout(timerId);
      timerId = setTimeout(() => {
        terakhirDijalankan = Date.now();
        timerId = null;
        fn(...args);
      }, sisaWaktu);
    }
  };
}

// contoh: update posisi scroll maksimal 10 kali per detik
const handleScroll = throttle((scrollY: number) => {
  console.log("Scroll position:", scrollY);
  // update UI berdasarkan posisi scroll
}, 100);

// simulasi banyak event scroll
for (let i = 0; i < 20; i++) {
  setTimeout(() => handleScroll(i * 50), i * 30); // scroll setiap 30ms
}
// handleScroll hanya dipanggil setiap 100ms meskipun event datang setiap 30ms

Timer dengan Presisi Tinggi #

Date.now() memiliki presisi milidetik, tapi untuk benchmarking atau pengukuran performa, gunakan process.hrtime.bigint() yang memiliki presisi nanodetik.

// Date.now() — presisi milidetik (ms)
const mulaiMs = Date.now();
await delay(100);
console.log(`Durasi: ${Date.now() - mulaiMs}ms`);

// process.hrtime.bigint() — presisi nanodetik (ns)
const mulaiNs = process.hrtime.bigint();
await delay(100);
const durasiNs = process.hrtime.bigint() - mulaiNs;
console.log(`Durasi: ${durasiNs}ns`);               // misal: 100234567ns
console.log(`Durasi: ${Number(durasiNs) / 1e6}ms`); // misal: 100.234567ms

// helper untuk benchmarking
async function ukurWaktu<T>(
  label: string,
  fn: () => Promise<T> | T
): Promise<T> {
  const mulai = process.hrtime.bigint();
  const hasil = await fn();
  const durasi = process.hrtime.bigint() - mulai;

  console.log(`[${label}] ${(Number(durasi) / 1e6).toFixed(3)}ms`);
  return hasil;
}

// bandingkan performa dua implementasi
await ukurWaktu("Array.reduce", () => {
  return Array.from({ length: 1_000_000 }, (_, i) => i).reduce((a, b) => a + b, 0);
});

await ukurWaktu("for loop", () => {
  let sum = 0;
  for (let i = 0; i < 1_000_000; i++) sum += i;
  return sum;
});

// timeout dengan presisi tinggi — berguna untuk SLA monitoring
async function denganTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number,
  label: string = "operasi"
): Promise<T> {
  const mulai = Date.now();

  const hasilPromise = fn();
  const timeoutPromise = delay(timeoutMs).then(() => {
    throw new Error(`${label} timeout setelah ${timeoutMs}ms`);
  });

  try {
    return await Promise.race([hasilPromise, timeoutPromise]);
  } finally {
    const durasi = Date.now() - mulai;
    if (durasi > timeoutMs * 0.8) {
      console.warn(`[WARN] ${label} butuh ${durasi}ms (mendekati timeout ${timeoutMs}ms)`);
    }
  }
}

Scheduler — Cron-like di Node.js #

Untuk penjadwalan tugas berulang pada waktu tertentu (setiap jam, setiap hari pada pukul 00:00, dll), hitung delay ke waktu berikutnya:

// hitung milidetik sampai waktu tertentu berikutnya
function msHingga(jam: number, menit: number = 0, detik: number = 0): number {
  const sekarang = new Date();
  const target = new Date();
  target.setHours(jam, menit, detik, 0);

  if (target <= sekarang) {
    // waktu hari ini sudah lewat — jadwalkan untuk besok
    target.setDate(target.getDate() + 1);
  }

  return target.getTime() - sekarang.getTime();
}

// jalankan task setiap hari pada waktu tertentu
function jadwalkanHarian(
  jam: number,
  menit: number,
  task: () => Promise<void>
): () => void {
  let timerId: ReturnType<typeof setTimeout>;
  let berjalan = true;

  async function jadwalBerikutnya(): Promise<void> {
    if (!berjalan) return;

    const delay = msHingga(jam, menit);
    console.log(`Task dijadwalkan dalam ${Math.round(delay / 1000 / 60)} menit`);

    timerId = setTimeout(async () => {
      if (!berjalan) return;
      try {
        await task();
      } catch (err) {
        console.error("Error dalam scheduled task:", err);
      }
      jadwalBerikutnya(); // jadwalkan untuk hari berikutnya
    }, delay);
  }

  jadwalBerikutnya();

  return () => {
    berjalan = false;
    clearTimeout(timerId);
  };
}

// contoh: kirim laporan harian setiap pukul 07:00
const hentikanLaporan = jadwalkanHarian(7, 0, async () => {
  console.log("Mengirim laporan harian...");
  // await kirimLaporan();
});

// hentikan saat aplikasi shutdown
process.on("SIGTERM", () => {
  hentikanLaporan();
});

Kapan Menggunakan Timer yang Mana #

setTimeout(fn, ms):
  ✓ Jalankan sekali setelah delay — animasi, notification, timeout
  ✓ Retry dengan backoff — tunggu sebelum mencoba ulang
  ✓ Debounce dan throttle — batasi frekuensi eksekusi
  ✓ Delay dalam pengujian — simulasi waktu tunggu

setInterval(fn, ms):
  ✓ Polling status secara periodik — cek health, update data
  ✓ Animasi frame sederhana (lebih baik requestAnimationFrame di browser)
  ✗ Callback asinkron — gunakan setTimeout rekursif agar tidak overlap

setImmediate(fn):
  ✓ Yield ke event loop di antara operasi CPU-intensive yang panjang
  ✓ Pastikan I/O callbacks diproses sebelum kode kamu berjalan
  ✓ Alternatif setTimeout(fn, 0) yang lebih deterministik di Node.js

queueMicrotask(fn):
  ✓ Batch update — kumpulkan perubahan dan proses sekali
  ✓ Jadwalkan sesuatu setelah kode sinkron saat ini tapi sebelum I/O
  ✗ Operasi I/O — microtask tidak boleh berisi operasi async yang panjang

Ringkasan #

  • Timer di Node.js adalah “minimum delay” — delay aktual bisa lebih lama jika event loop sedang sibuk; jangan andalkan ketepatan waktu milidetik untuk logika kritis.
  • Bungkus setTimeout dalam Promise dengan fungsi delay(ms) agar bisa digunakan dengan async/await — jauh lebih bersih dari callback bertingkat.
  • Gunakan setTimeout rekursif, bukan setInterval, untuk callback asinkron — setInterval tidak menunggu callback selesai sehingga eksekusi bisa menumpuk jika callback lebih lambat dari interval.
  • setImmediate untuk yield ke event loop di antara operasi CPU-intensive yang panjang — ini mencegah server tidak responsif terhadap request I/O yang masuk.
  • queueMicrotask untuk batch update — jadwalkan flush di akhir kode sinkron saat ini; berguna untuk menggabungkan banyak perubahan kecil menjadi satu operasi.
  • Debounce untuk input pengguna — tunda eksekusi sampai pengguna berhenti; throttle untuk event frekuensi tinggi seperti scroll — jalankan paling banyak sekali per interval.
  • process.hrtime.bigint() untuk pengukuran performa yang akurat — presisi nanodetik jauh lebih baik dari Date.now() untuk benchmarking.
  • Selalu simpan referensi timer dari setTimeout/setInterval dan panggil clearTimeout/clearInterval di cleanup — timer yang tidak dibatalkan adalah sumber memory leak yang umum.

← Sebelumnya: Events   Berikutnya: Date & Time →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact