Ketika semua percobaan retry gagal dan respons yang tiba berstatus 404 atau 422, tugas JavaScript belum selesai — justru dimulai. Menampilkan kode status mentah kepada pengguna (“Error 404”) tidak membantu siapa pun; menelan galatnya dalam diam membuat Siti mengira pembayarannya berhasil padahal tidak. Penanganan galat API yang baik berdiri di antara keduanya: membaca detail teknis dari server, lalu menerjemahkannya menjadi pesan yang jujur dan mudah dipahami.
Chapter penutup section ini menyusun polanya utuh: struktur respons galat yang lazim, class ApiError yang membawa status dan isi galat, serta pemetaan pesan ramah untuk pengguna. Server tiruannya tertanam di berkas contoh; node kelas-apierror.mjs menghasilkan keluaran yang sama persis dengan yang tercetak di bawah.
Struktur Respons Galat API #
API modern jarang mengirim galat sebagai teks kosong. Konvensi yang paling umum: status menempatkan galat pada keluarganya (4xx salah di sisi client, 5xx salah di sisi server), sedangkan body menjelaskan secara terstruktur — biasanya sebuah object dengan kode galat yang stabil dan pesan yang terbaca:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "PRODUK_TIDAK_DITEMUKAN",
"message": "Buku dengan id 99 tidak ada dalam katalog."
}
}Dua bagian itu punya pembaca berbeda. code bersifat stabil dan ditulis UPPERCASE_SNAKE — dibandingkan program, tidak pernah berubah tanpa pengumuman. message ditujukan untuk developer yang membaca log; isi boleh berubah dan sering berbahasa teknis. Pemisahan ini penting karena teks message tidak layak dijadikan syarat if di kode client — yang layak adalah code.
Bentuk lain yang juga lazim dijumpai: respons validasi membawa details berupa array berisi satu entri per field yang salah, sehingga formulir bisa menandai semuanya sekaligus. Struktur persisnya adalah kesepakatan antara API dan client-nya; yang penting kontraknya konsisten, karena seluruh penanganan galat kita akan menggantung padanya.
class ApiError: Galat yang Membawa Konteks #
Pola if (!response.ok) throw new Error(...) dari chapter Response Status sudah memindahkan kendali ke tangan kita, tetapi Error biasa hanya punya message. Bila penanganan di lapisan atas perlu tahu statusnya berapa dan isi body galatnya apa — untuk membedakan “coba lagi” dari “tampilkan halaman tidak ditemukan” — kita butuh galat khusus. Pewarisan class dari section Object-Oriented JavaScript dipakai persis untuk keperluan ini:
// kelas-apierror.mjs
// class ApiError: membawa status dan body galat dari API ke penanganan kita.
// Jalankan di terminal: node kelas-apierror.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({ error: { code: "PRODUK_TIDAK_DITEMUKAN", message: "Buku dengan id 99 tidak ada dalam katalog." } }));
return;
}
if (req.url === "/pesanan") {
res.writeHead(422, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { code: "VALIDASI_GAGAL", message: "Field 'email' harus berformat email." } }));
return;
}
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { code: "SERVER_SIBUK", message: "Coba beberapa saat lagi." } }));
});
// Galat khusus API: menyimpan status HTTP dan isi body galat.
class ApiError extends Error {
constructor(status, body) {
super(body?.error?.message ?? "Respons tidak standar dari server");
this.name = "ApiError";
this.status = status;
this.body = body;
}
}
// Helper: semua pemanggil fetch melewatinya agar pengecekan tidak ditulis ulang.
async function ambil(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new ApiError(response.status, body);
}
return response;
}
// Peta kode galat API -> pesan ramah untuk pengguna.
const pesanRamah = new Map([
["PRODUK_TIDAK_DITEMUKAN", "Produk yang dicari tidak tersedia. Mungkin sudah dihapus dari katalog."],
["VALIDASI_GAGAL", "Periksa kembali isian formulir."],
]);
async function tampilkan(label, promise) {
try {
const response = await promise;
console.log(`${label}: sukses ${response.status} -`, JSON.stringify(await response.json()));
} catch (error) {
if (error instanceof ApiError) {
const ramah = pesanRamah.get(error.body?.error?.code) ?? "Terjadi kegagalan di server. Coba beberapa saat lagi.";
console.log(`${label}: [${error.status}] ${ramah}`);
console.log(`${label}: log developer -> ${error.name} ${error.status} ${error.body.error.code}`);
} else {
console.log(`${label}: galat tak terduga -`, error.message);
}
}
}
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
const dasar = `http://127.0.0.1:${port}`;
await tampilkan("buku/1 ", ambil(`${dasar}/buku/1`));
await tampilkan("buku/99 ", ambil(`${dasar}/buku/99`));
await tampilkan("pesanan ", ambil(`${dasar}/pesanan`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "budi-contoh" }),
}));
server.closeAllConnections();
server.close();
});Output yang Dihasilkan:
buku/1 : sukses 200 - {"id":1,"judul":"Seyum Membaca"}
buku/99 : [404] Produk yang dicari tidak tersedia. Mungkin sudah dihapus dari katalog.
buku/99 : log developer -> ApiError 404 PRODUK_TIDAK_DITEMUKAN
pesanan : [422] Periksa kembali isian formulir.
pesanan : log developer -> ApiError 422 VALIDASI_GAGALMari kita telaah baris-baris pentingnya:
- Baris 28–35 (class ApiError): constructor menyimpan tiga hal —
super(...)meneruskan pesan terbaik yang bisa diambil dari body keError,this.name = "ApiError"membuat pelaporan galat menyebut nama kelasnya (bukan “Error” generik), laluthis.statusdanthis.bodymenyimpan konteks yang tidak dimilikiErrorbiasa. - Baris 41 (
response.json().catch(() => null)): body galat tidak selalu JSON — server yang sedang susah bisa mengirim halaman HTML..catch(() => null)memastikan helper tetap melemparApiErrorberisibody: nullalih-alih gagal kedua kalinya denganSyntaxError. - Baris 40–42 (titik lempar tunggal): seluruh pemeriksaan
response.okkumpul di helperambil. Setiap pemanggil di aplikasi mewarisi aturan yang sama tanpa menulis ulang — satu tempat perbaikan, semua pemanggil ikut membaik. - Baris 48–51 (peta pesan ramah):
Mapmenghubungkancodeyang stabil dengan kalimat untuk pengguna. Menambah galat baru berarti menambah satu baris di sini, bukan menyebarifbaru di banyak file. - Baris 58 (
instanceof ApiError): pemeriksaan jenis galat dulu, baru isi. Galat dariApiErrorpunya jalannya sendiri; galat lain — misalnyaTypeError: fetch faileddari jaringan yang terputus — jatuh ke cabang “galat tak terduga”. - Baris 59 (
pesanRamah.get(...) ?? ...): kode yang belum terdaftar di peta tidak membuat aplikasi bingung; operator??menyediakan pesan cadangan yang aman. Ini penting karena server bisa memperkenalkan kode baru kapan pun. - Keluaran dua baris per galat: baris pertama untuk pengguna — status dan pesan ramah; baris kedua untuk log developer —
ApiError 404 PRODUK_TIDAK_DITEMUKAN. Keduanya lahir dari object galat yang sama, tanpa duplikasi permintaan.
Menerjemahkan Galat Menjadi Tindakan #
Pesan ramah adalah lapisan terakhir; di atasnya, jenis galat menentukan tindakan aplikasi. Pola keputusan yang lazim:
status 404→ tampilkan halaman “data tidak ditemukan”, bukan galat merah.status 401→ arahkan pengguna ke halaman login, karena sesi habis atau belum masuk; jangan mencoba ulang permintaan yang sama.status 403→ sesi sah tetapi akses tidak diizinkan — pesannya berbeda dari401: ini bukan soal login, melainkan soal hak.status 422→ tampilkan kembali formulir dengan penanda pada field yang salah;body.errordari API validasi biasanya merinci field-nya.status 429atau5xx→ tawarkan tombol coba lagi; kombinasi ini nyaris selalu cocok dipasangkan dengan retry dari chapter sebelumnya.- Galat jaringan (
TypeError: fetch failed) → bedakan pesannya dari galat server: “koneksi bermasalah” meminta pengguna memeriksa internet, bukan menyalahkan aplikasi.
Sisi logging juga punya tata kelolanya. console.error lebih tepat daripada console.log untuk galat karena menyasar kanal log yang benar, dan isi yang dicetak sebaiknya konteks teknis — status, code, X-Request-Id dari chapter Headers — bukan pesan ramah yang sudah ditampilkan ke pengguna. Aplikasi yang serius meneruskan galat terstruktur ini ke layanan pemantauan, sehingga lonjakan SERVER_SIBUK terlihat di dasbor tim server sebelum pengguna mulai berteriak di kanal dukungan.
Dengan ini, rangkaian percakapan HTTP kita lengkap: dari anatomi request-response, fetch dan metodenya, pembacaan status dan header, aturan lintas origin, kontrol waktu dengan AbortController, penelusuran data bervolume besar, hingga ketahanan lewat retry dan penerjemahan galat. Keterampilan-keterampilan ini adalah fondasi setiap aplikasi JavaScript yang berbicara dengan server — dan selanjutnya tinggal dipraktikkan pada proyek.