Saat aplikasi menampilkan “Terjadi kesalahan, coba lagi” tanpa penjelasan, hampir bisa dipastikan penulisnya lupa membaca kode status respons. Status 404 dan 500 mengandung informasi berharga — satu mengatakan data tidak ada, yang lain mengatakan server sedang bermasalah — tetapi keduanya baru berguna bila kode kita tahu cara membacanya. Chapter ini melatih pembacaan itu: keluarga status, properti response.ok dan response.status, serta satu perilaku fetch yang mengejutkan hampir semua pemula.

Seperti biasa, kedua berkas contoh menyertakan server tiruan tertanam, jadi node periksa-status.mjs dan node res-tak-dijangkau.mjs bisa langsung Anda jalankan untuk menghasilkan keluaran yang sama.

Empat Keluarga yang Perlu Dikenal #

Digit pertama kode status mengelompokkan nasib permintaan:

  • 2xx — sukses. 200 OK untuk pembacaan dan perubahan biasa, 201 Created setelah POST berhasil, 204 No Content saat server sukses tetapi tak punya isi untuk dikirim.
  • 3xx — pengalihan. 301 (pindah permanen) dan 302 (sementara) menunjuk alamat baru lewat header Location; 304 Not Modified memberi tahu cache masih layak dipakai.
  • 4xx — kesalahan client. 400 payload tidak sah, 401 belum terautentikasi, 403 terautentikasi tetapi tidak diizinkan, 404 sumber daya tidak ada, 422 data tidak lolos validasi, 429 terlalu banyak permintaan.
  • 5xx — kesalahan server. 500 kegagalan internal, 502 gateway salah balas, 503 server sementara tidak siap.

Batasnya menegaskan siapa yang perlu bertindak: 4xx berarti permintaan perlu diperbaiki oleh pengirim, 5xx berarti pihak server yang bekerja memulihkan layanannya.

fetch Tidak Melempar Galat untuk 404 atau 500 #

Intuisi banyak pemula: “status galat pasti membuat fetch reject”. Kenyataannya tidak. fetch menyelesaikan Promise-nya (resolve) dengan sukses sepanjang server membalas — apa pun statusnya. Status 404 adalah balasan sah dari server; jaringan bekerja baik, hanya isinya berita buruk.

Berkas berikut membuktikannya dengan tiga rute server tiruan: satu sukses, satu 404, dan satu 500:

// periksa-status.mjs
// fetch TIDAK melempar galat untuk 404/500; kita memeriksa response.ok sendiri.
// Jalankan di terminal: node periksa-status.mjs (tanpa internet).

import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.url === "/buku/1") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ id: 1, judul: "Seyum Membaca" }));
    return;
  }
  if (req.url === "/buku/99") {
    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ pesan: "Buku dengan id 99 tidak ada" }));
    return;
  }
  if (req.url === "/server-gagal") {
    res.writeHead(500, { "Content-Type": "text/plain" });
    res.end("kesalahan internal server tiruan");
    return;
  }
  if (req.url === "/alamat-lama") {
    res.writeHead(301, { Location: "/alamat-baru" });
    res.end();
    return;
  }
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("halaman baru");
});

server.listen(0, "127.0.0.1", async () => {
  const port = server.address().port;
  const dasar = `http://127.0.0.1:${port}`;

  for (const alamat of ["/buku/1", "/buku/99", "/server-gagal"]) {
    const response = await fetch(`${dasar}${alamat}`);
    console.log(
      `${alamat.padEnd(14)} status=${response.status} ok=${response.ok} type=${response.headers.get("content-type")}`
    );
  }

  // Pola pengecekan: fetch tetap resolve, jadi kita yang memutuskan lempar galat.
  try {
    const response = await fetch(`${dasar}/buku/99`);
    if (!response.ok) {
      throw new Error(`Permintaan gagal dengan status ${response.status}`);
    }
    console.log("sampai sini:", await response.json());
  } catch (error) {
    console.log("tertangkap:", error.message);
  }

  // 3xx tidak muncul sebagai status akhir: fetch mengikutinya secara otomatis.
  const responsePindah = await fetch(`${dasar}/alamat-lama`);
  console.log("redirect: status", responsePindah.status, "| redirected:", responsePindah.redirected);

  server.closeAllConnections();
  server.close();
});

Output yang Dihasilkan:

text
/buku/1        status=200 ok=true type=application/json
/buku/99       status=404 ok=false type=application/json
/server-gagal  status=500 ok=false type=text/plain
tertangkap: Permintaan gagal dengan status 404
redirect: status 200 | redirected: true

Mari kita telaah baris-baris pentingnya:

  1. Baris 36–41 (perulangan tiga alamat): fetch dipanggil tiga kali tanpa try-catch, dan ketiganya selesai tanpa galat. Yang membedakan hanya response.status dan response.ok — ok bernilai true hanya untuk rentang status 200–299.
  2. Baris 45–47 (pola pengecekan): karena fetch tidak akan memutuskan untuk kita, kita yang memeriksa if (!response.ok) dan melempar Error sendiri, lengkap dengan statusnya di pesan. Baris console.log di dalam try tidak pernah tercapai untuk /buku/99.
  3. Baris 50–51 (penangkapan): galat buatan kita tertangkap di catch — pola yang sama akan kita angkat menjadi class ApiError di chapter API Error Handling.
  4. Baris 23–26 (rute 301): server menjawab 301 dengan header Location: /alamat-baru. Perhatikan keluaran redirect: status akhirnya 200, bukan 301, dan response.redirected bernilai true — fetch mengikuti pengalihan secara otomatis sampai respons final diperoleh.

Kegagalan Jaringan: Satu-satunya Rejection #

Kapan fetch benar-benar reject? Ketika permintaan tidak pernah mendapat balasan: alamat tidak terjangkau, koneksi putus di tengah jalan, DNS gagal, atau server sedang tidak menyala. Dalam kasus itu fetch menolak dengan TypeError berpesan "fetch failed".

Berkas kedua membangun situasi itu secara deterministik: server dinyalakan sekadar meminjam satu port kosong, lalu dimatikan, sehingga port itu terlantar tanpa penjaga. fetch ke alamatnya pun pasti ditolak:

// res-tak-dijangkau.mjs
// Satu-satunya kasus di mana fetch melempar galat: jaringan yang tidak terjangkau.
// Jalankan di terminal: node res-tak-dijangkau.mjs (tanpa internet).

import http from "node:http";

// 1. Nyalakan server hanya untuk mendapat satu port kosong.
const server = http.createServer(() => {});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;

// 2. Matikan server; portnya kini tidak dijaga siapa pun.
server.close();
await new Promise((resolve) => server.on("close", resolve));
console.log("port acak itu kini kosong — tidak ada yang mendengarkan.");

// 3. fetch ke port kosong: permintaan tidak pernah sampai ke mana pun.
try {
  await fetch(`http://127.0.0.1:${port}/`);
} catch (error) {
  console.log("nama  :", error.name);
  console.log("pesan :", error.message);
  console.log("sebab :", error.cause?.code);
}

Output yang Dihasilkan:

text
port acak itu kini kosong — tidak ada yang mendengarkan.
nama  : TypeError
pesan : fetch failed
sebab : ECONNREFUSED

Mari kita telaah baris-baris pentingnya:

  1. Baris 9 (await new Promise(... server.listen(0, ...))): listen dibungkus Promise agar top-level await menunggu port benar-benar terbuka; angka 0 kembali dipakai untuk mendapat port acak yang pasti kosong.
  2. Baris 13–14 (server.close()): server berhenti mendengarkan, dan await kedua menunggu penutupan selesai. Mulai titik ini, port tersebut tidak lagi dijaga siapa pun.
  3. Baris 19 (fetch(...)): permintaan dikirim ke port yang kosong. Sistem operasi langsung menolak sambungannya, dan fetch menolak dengan TypeError.
  4. Baris 21–23 (baca galat): error.name adalah "TypeError" dan error.message selalu "fetch failed" — generik memang. Detail aslinya diselamatkan di error.cause: kode ECONNREFUSED (sambungan ditolak) memberi petunjuk teknis yang jauh lebih berguna saat mendiagnosis.

Karena penolakan jenis ini berbentuk lain dari “respons buruk”, pola penanganan yang sehat menggabungkan keduanya: try-catch untuk kegagalan jaringan, if (!response.ok) untuk status galat. Chapter Retry dan Timeout Strategies memanfaatkan perbedaan itu untuk memutuskan mana yang layak diulang — dan chapter berikutnya menyingkap peran header dalam semua percakapan ini.

Sebagai catatan penutup bagian ini: statusText (“OK”, “Not Found”) hanyalah keterangan pelengkap dari server, dan banyak HTTP/2 respons memang tidak membawanya — jangan pernah menjadikannya dasar logika atau pesan untuk pengguna. Angka status dan boolean ok adalah dua-duanya yang bisa diandalkan.

Merancang Status untuk Server Sendiri #

Karena seluruh berkas contoh di section ini membangun server tiruan sendiri, kita sesungguhnya sedang berlatih memilih kode status — keterampilan yang sama nantinya dipakai saat membangun API sungguhan. Beberapa panduan yang sudah kita terapkan di server-server tadi:

  • Baca data → 200; buat data baru → 201; sukses tanpa isi → 204. Mengirim 200 dengan body kosong untuk operasi hapus membuat client menebak — 204 menyatakan maksud itu dengan tepat.
  • Data tidak ada → 404, bukan 200 berisi null. Client yang memeriksa response.ok mengandalkan kesetiaan pemetaan ini.
  • Payload tidak bisa diurai → 400; payload sah tetapi isinya gagal validasi → 422. Keduanya sering tertukar, dan keduanya tetap lebih jujur daripada 500 untuk masalah yang berasal dari client.
  • Server sementara tidak siap → 503, bukan 500. Angka 503 menyatakan “kondisi ini bisa pulih” — sinyal yang benar bagi mesin retry.

Untuk 429 dan 503, server yang baik sering menyertakan header Retry-After yang mengajukan jeda tunggu — petunjuk yang akan berguna saat kita menyusun strategi retry di chapter berikutnya. Kesesuaian status dengan keadaan sebenarnya inilah yang memungkinkan client ditulis sederhana: cukup percaya pada response.ok dan keluarganya.