Katalog toko buku Budi diambil dari API penyedia data. Selama penyedianya rapi, semuanya lancar — sampai suatu saat respons datang dengan kunci buku bernilai null, atau daftar berisi item tanpa judul. Program yang selama ini mengambil data mentah lalu langsung memakainya berhenti dengan TypeError di tempat yang jauh dari sumber masalahnya.
Data dari luar program — respons API, berkas, pesan — tidak bisa kita kendalikan bentuknya. Yang bisa kita kendalikan adalah cara program kita menerima: periksa struktur lebih dulu, beri nilai default untuk bagian yang hilang, dan pastikan bentuk datanya seragam sebelum dipakai. Cara mengirim permintaannya (fetch) dibahas lengkap di section Network dan API; chapter ini berfokus pada pengolahannya, dengan data tiruan yang bentuknya meniru respons nyata.
Asumsi yang Mudah Pecah #
Kebiasaan “langsung pakai” biasanya ditulis seperti ini, dan kodenya memang sah — sampai data berubah bentuk:
const respon = { data: null };
try {
console.log(respon.data.buku);
} catch (galat) {
console.log(galat.name + ": " + galat.message);
}Output yang Dihasilkan:
TypeError: Cannot read properties of null (reading 'buku')Kode di atas berasumsi respon.data selalu berisi object. Ketika penyedia mengirim data: null — misalnya saat kategori sedang kosong — pembacaan respon.data.buku melempar TypeError. Masalahnya berlapis: galat muncul jauh setelah data diterima, di baris yang mengira dirinya aman karena “responsnya sudah masuk”.
Guard Clause, Nilai Default, dan Normalisasi #
Tiga langkah pengamanan itu dikerjakan bersama dalam satu function. Berkas berikut menata data katalog dari dua respons dengan bentuk berbeda — satu lengkap, satu dengan buku: null — ditambah percobaan tanpa respons sama sekali. Tiga janji yang dibawa keluar function ini: hasilnya selalu array, setiap item punya id, judul, dan stok, dan tidak ada satu pun langkah yang melempar galat bentuk:
// olah-katalog.js — mengamankan data "hasil API" sebelum dipakai
// Data tiruan dengan bentuk seolah-olah datang dari server:
// fetch sendiri dibahas di section Network dan API.
const responFiksi = {
status: "ok",
data: {
kategori: "Fiksi",
buku: [
{ id: 1, judul: "Cerita Kopi Nusantara", stok: 12 },
{ id: 2, judul: "Rahasia Gula Aren", stok: null },
{ id: 3, judul: null, stok: 3 },
],
},
};
const responNonfiksi = { status: "ok", data: { kategori: "Nonfiksi", buku: null } };
// --- 1. guard clause + nilai default + normalisasi array ---
function normalisasiKatalog(respon) {
if (!respon || !respon.data) {
return [];
}
const daftar = Array.isArray(respon.data.buku) ? respon.data.buku : [];
return daftar.map((buku) => ({
id: buku.id,
judul: buku.judul ?? "Tanpa judul",
stok: buku.stok ?? 0,
}));
}
console.log(normalisasiKatalog(responFiksi));
console.log(normalisasiKatalog(responNonfiksi));
console.log(normalisasiKatalog(null));
// --- 2. cache sederhana dengan Map ---
const cache = new Map();
function ambilKategori(nama, muatData) {
if (cache.has(nama)) {
console.log("cache hit: " + nama);
return cache.get(nama);
}
console.log("cache miss: " + nama);
const hasil = muatData();
cache.set(nama, hasil);
return hasil;
}
ambilKategori("Fiksi", () => normalisasiKatalog(responFiksi));
ambilKategori("Fiksi", () => normalisasiKatalog(responFiksi));
ambilKategori("Nonfiksi", () => normalisasiKatalog(responNonfiksi));
console.log(cache.size);Output yang Dihasilkan:
[
{ id: 1, judul: 'Cerita Kopi Nusantara', stok: 12 },
{ id: 2, judul: 'Rahasia Gula Aren', stok: 0 },
{ id: 3, judul: 'Tanpa judul', stok: 3 }
]
[]
[]
cache miss: Fiksi
cache hit: Fiksi
cache miss: Nonfiksi
2Mari kita telaah baris-baris pentingnya:
- Baris 21–23 (guard clause):
if (!respon || !respon.data)adalah guard clause — syarat yang mengembalikan program lebih awal bila bentuk dasarnya sudah salah.nullmaupun respons tanpadatalangsung menghasilkan array kosong, bukanTypeError. Array kosong dipilih karena kode pemanggil biasa menelusuri daftar: daftar kosong aman ditelusuri,nulltidak. - Baris 24 (normalisasi array):
Array.isArray(respon.data.buku)memastikan nilai benar-benar array sebelum dipakai. ResponsNonfiksidatang denganbuku: nulldan tetap menghasilkan[]— bentuk keluaran function ini selalu array, apa pun masukannya. - Baris 25–29 (nilai default dengan
??): setiap field kritis diberi cadangan dengan operator??. Item ke-2 yangstok-nyanullmenjadistok: 0, dan item ke-3 tanpa judul menjadi"Tanpa judul". Hasil akhirnya: array yang bentuknya seragam dan bisa dihitung, diurutkan, atau ditampilkan tanpa pemeriksaan tambahan. - Baris 34 (
normalisasiKatalog(null)): kasus terburuk pun — tidak ada respons — hanya menghasilkan[]. Function ini tidak punya jalur yang bisa melempar galat bentuk. - Baris 39–48 (cache dengan Map):
ambilKategorimenerima nama kategori dan function pemuat.cache.has(nama)memeriksa apakah kategori pernah diambil; bila ya, cukupcache.get(nama)tanpa memuat ulang. Bila tidak,muatData()dijalankan sekali dan hasilnya disimpan dengancache.set(nama, hasil). - Baris 50–53 (uji hit dan miss): panggilan
Fiksipertama mencatatcache miss, panggilanFiksikeduacache hit— data sama tanpa kerja ulang — danNonfiksikembalicache miss.cache.sizebernilai2, jumlah kategori yang tersimpan.
?? daripada || untuk Nilai Default #
Pada baris 27–28, nilai default diberi lewat ??, bukan ||. Bedanya terasa pada nilai yang “palsu” tetapi sah:
const stok = 0;
console.log(stok ?? 10);
console.log(stok || 10);Output yang Dihasilkan:
0
10Operator || menganggap 0 kosong dan menggantinya dengan 10 — padahal nol adalah stok yang sah. ?? hanya mengganti null dan undefined, sehingga 0, string kosong, dan false tetap dihormati. Untuk data dari luar yang nilainya bisa apa pun, ?? adalah pilihan yang jauh lebih aman.
Cache Sederhana dengan Map #
Pola cache pada berkas utama memakai Map karena dua sifatnya cocok: kuncinya bisa string apa pun (nama kategori, ID produk), dan pencarian has/get berjalan cepat tanpa menyapu isi. Polanya selalu sama — periksa, lalu muat bila belum ada:
cache.has(nama): pertanyaan “sudah pernah diambil?” dijawab tanpa kerja berat;cache.get(nama): mengambil hasil yang tersimpan;cache.set(nama, hasil): menyimpan hasil muatan pertama agar panggilan berikutnya mengenai cache hit.
Function muatData pada contoh ini memanggil normalisasiKatalog secara langsung. Di aplikasi nyata, function yang sama akan memanggil API — dan bila pemuatnya berupa function async, bentuk ambilKategori tidak perlu berubah: hasilnya yang nanti berupa Promise, dan pembahasannya menyambung tepat ke section Network dan API.
Dua keputusan desain menentukan apakah cache seperti ini membantu atau malah menyesatkan:
- Kunci harus mewakili seluruh parameter muatan. Bila hasil tergantung pada dua hal — misalnya kategori dan halaman — kuncinya harus memuat keduanya (
"Fiksi"vs"Fiksi:halaman-2"). Kunci yang kurang lengkap membuat dua permintaan berbeda berbagi jawaban yang salah. - Data yang jarang berubah lebih layak di-cache. Daftar kategori stabil boleh tersimpan; stok yang berubah tiap penjualan lebih aman dimuat ulang. Cache yang tidak pernah diperbarui akan menyajikan data usang dengan percaya diri.
Sampai di sini, rangkaian section Data dan Serialization utuh: JSON untuk teks pertukaran data, structuredClone untuk salinan yang setia, Date dan Intl untuk waktu serta tampilan lokal, penyandian URL untuk data di dalam tautan, dan di akhir — benteng penerimaan data dari luar. Langkah berikutnya membawa pola-pola ini ke jaringan sungguhan: section Network dan API, tempat fetch, header, dan status respons dibahas satu per satu.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
olah-katalog.js
Unduh berkas// olah-katalog.js — mengamankan data "hasil API" sebelum dipakai
// Data tiruan dengan bentuk seolah-olah datang dari server:
// fetch sendiri dibahas di section Network dan API.
const responFiksi = {
status: "ok",
data: {
kategori: "Fiksi",
buku: [
{ id: 1, judul: "Cerita Kopi Nusantara", stok: 12 },
{ id: 2, judul: "Rahasia Gula Aren", stok: null },
{ id: 3, judul: null, stok: 3 },
],
},
};
const responNonfiksi = { status: "ok", data: { kategori: "Nonfiksi", buku: null } };
// --- 1. guard clause + nilai default + normalisasi array ---
function normalisasiKatalog(respon) {
if (!respon || !respon.data) {
return [];
}
const daftar = Array.isArray(respon.data.buku) ? respon.data.buku : [];
return daftar.map((buku) => ({
id: buku.id,
judul: buku.judul ?? "Tanpa judul",
stok: buku.stok ?? 0,
}));
}
console.log(normalisasiKatalog(responFiksi));
console.log(normalisasiKatalog(responNonfiksi));
console.log(normalisasiKatalog(null));
// --- 2. cache sederhana dengan Map ---
const cache = new Map();
function ambilKategori(nama, muatData) {
if (cache.has(nama)) {
console.log("cache hit: " + nama);
return cache.get(nama);
}
console.log("cache miss: " + nama);
const hasil = muatData();
cache.set(nama, hasil);
return hasil;
}
ambilKategori("Fiksi", () => normalisasiKatalog(responFiksi));
ambilKategori("Fiksi", () => normalisasiKatalog(responFiksi));
ambilKategori("Nonfiksi", () => normalisasiKatalog(responNonfiksi));
console.log(cache.size);