Tipe Penanganan Error

unknown pada blok catch dan pola bergaya Result untuk kegagalan.

Daftar isi

Kegagalan pada JavaScript dapat dilaporkan dengan dua cara, yaitu nilai yang dilempar dengan throw dan kegagalan yang dinyatakan melalui tipe kembalian. Topik ini membahas tipe untuk keduanya, yaitu variabel blok catch yang bertipe unknown, sifat nilai lemparan yang bebas, serta pola bergaya Result yang menjadikan kegagalan bagian dari tipe kembalian. Setelah membacanya, kegagalan dapat ditangani dengan pemeriksaan yang dipaksakan compiler, bukan dengan asumsi.

unknown pada blok catch#

Variabel pada blok catch ditetapkan bertipe unknown, karena compiler tidak mengetahui apa yang akan dilempar oleh kode di dalam blok try. Akibatnya, properti seperti message tidak dapat diakses langsung pada variabel tersebut. Nilai itu harus dipersempit lebih dahulu, misalnya dengan instanceof Error, sebelum properti objek error boleh dibaca.

ts catch-unknown.ts
function bacaNilai(): number {
  throw new Error("data belum tersedia");
}

try {
  const nilai = bacaNilai();
  console.log(nilai);
} catch (e) {
  console.log(e instanceof Error ? e.message : String(e));
}
text Keluaran
data belum tersedia

Pemanggilan bacaNilai() melempar objek Error, sehingga eksekusi melompat ke blok catch. Ekspresi e instanceof Error ? e.message : String(e) mempersempit e menjadi Error pada cabang pertama, sedangkan cabang kedua mengubah nilai apa pun menjadi teks. Kedua cabang menghasilkan string, sehingga hasilnya aman dicetak tanpa satu pun asumsi tentang bentuk e.

Percobaan membaca properti tanpa pempersempitan ditolak compiler, sebagaimana berkas berikut yang sengaja memuat kesalahan tersebut.

ts catch-pakai-langsung.ts
// tidak-dijalankan
try {
  JSON.parse("{");
} catch (e) {
  console.log(e.message);
}
bash
# membuktikan pesan error untuk akses properti atas variabel catch
tsc --noEmit --pretty false catch-pakai-langsung.ts
text Keluaran
catch-pakai-langsung.ts(5,15): error TS18046: 'e' is of type 'unknown'.

Nilai yang dilempar tidak selalu Error#

Perintah throw menerima nilai apa pun, sehingga kegagalan dapat berupa string, angka, atau objek biasa, bukan hanya objek Error. Tipe unknown pada variabel catch menyatakan kebebasan itu dengan tepat. Kode lama yang melempar string masih banyak dijumpai, dan compiler tidak dapat memaksakan konvensi pada kode yang sudah ditulis.

ts lempar-string.ts
function cariPeserta(id: number): string {
  if (id !== 2) {
    throw "peserta tidak ditemukan";
  }
  return "Dewi";
}

try {
  console.log(cariPeserta(2));
  console.log(cariPeserta(7));
} catch (e) {
  if (typeof e === "string") {
    console.log(e);
  } else if (e instanceof Error) {
    console.log(e.message);
  } else {
    console.log("kegagalan tidak dikenal");
  }
}
text Keluaran
Dewi
peserta tidak ditemukan

Pemanggilan pertama berhasil dan mencetak Dewi, sedangkan pemanggilan kedua melempar string yang ditangkap blok catch. Pempersempitan dilakukan berurutan, yaitu typeof e === "string" menangani nilai lemparan berupa teks, lalu e instanceof Error menangani objek error. Cabang terakhir berfungsi sebagai pengaman untuk nilai lemparan yang bukan string maupun objek Error. Contoh ini menunjukkan mengapa unknown lebih tepat daripada Error, karena nilai lemparan memang tidak dijanjikan bertipe tertentu.

Pola bergaya Result#

Pola Result menyatakan kegagalan di dalam tipe kembalian, bukan melalui mekanisme lempar. Tipenya berupa union diskriminatif, yaitu gabungan beberapa bentuk objek yang dibedakan oleh satu bidang penanda. Penandanya adalah ok, sehingga Result<T, E> berarti { ok: true; nilai: T } untuk keberhasilan dan { ok: false; error: E } untuk kegagalan. Dua helper kecil bernama ok dan err membangun nilainya, dan parameter tipe never memastikan masing-masing helper hanya menghasilkan satu bentuk.

ts hasil-nilai.ts
type Result<T, E> = { ok: true; nilai: T } | { ok: false; error: E };

function ok<T>(nilai: T): Result<T, never> {
  return { ok: true, nilai };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

function parseNilai(teks: string): Result<number, string> {
  const angka = Number(teks);
  if (Number.isNaN(angka)) {
    return err(`"${teks}" bukan angka`);
  }
  if (angka < 0 || angka > 100) {
    return err(`nilai ${angka} di luar rentang 0 sampai 100`);
  }
  return ok(angka);
}

const hasilA = parseNilai("87");
const hasilB = parseNilai("abc");
const hasilC = parseNilai("150");

console.log(hasilA);
console.log(hasilB);
console.log(hasilC);

for (const hasil of [hasilA, hasilB, hasilC]) {
  if (hasil.ok) {
    console.log(`diterima: ${hasil.nilai}`);
  } else {
    console.log(`ditolak: ${hasil.error}`);
  }
}
text Keluaran
{ ok: true, nilai: 87 }
{ ok: false, error: '"abc" bukan angka' }
{ ok: false, error: 'nilai 150 di luar rentang 0 sampai 100' }
diterima: 87
ditolak: "abc" bukan angka
ditolak: nilai 150 di luar rentang 0 sampai 100

Fungsi parseNilai mengembalikan Result<number, string>, sehingga kegagalan penguraian teks menjadi angka tercantum langsung pada tanda tangannya. Tiga pemanggilan menghasilkan tiga bentuk yang berbeda, yaitu satu keberhasilan dan dua kegagalan dengan pesan yang berbeda. Perulangan di bagian akhir mempersempit union lewat pemeriksaan hasil.ok, sehingga hasil.nilai hanya dibaca pada cabang yang benar. Pola semacam ini memindahkan tanggung jawab menangani kegagalan dari konvensi menjadi aturan tipe.

Tipe never pada kedua helper bukan sekadar hiasan. Karena never tidak memiliki anggota, helper ok tidak mungkin menghasilkan bentuk err, dan sebaliknya. Konsekuensinya, pemeriksaan hasil.ok satu kali sudah cukup untuk menentukan bidang mana yang sah dibaca.

Membungkus fungsi asinkron dengan Result#

Banyak fungsi asinkron bawaan melaporkan kegagalan dengan melempar, dan pola Result tetap dapat dipakai untuk membungkusnya. Cara standarnya adalah pembungkus yang menjalankan tugas di dalam try, mengembalikan bentuk ok saat berhasil, serta mengubah nilai lemparan menjadi bentuk err saat gagal. Tipenya adalah jalankanAman<T>(tugas: () => Promise<T>): Promise<Result<T, string>>, sehingga tanda tangannya menjamin tidak ada pelemparan yang lolos.

ts jalankan-aman.ts
type Result<T, E> = { ok: true; nilai: T } | { ok: false; error: E };

async function jalankanAman<T>(tugas: () => Promise<T>): Promise<Result<T, string>> {
  try {
    return { ok: true, nilai: await tugas() };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : String(e) };
  }
}

async function bacaRuangA(): Promise<number> {
  throw new Error("sensor ruang A offline");
}

async function bacaRuangB(): Promise<number> {
  return 25;
}

const hasilA = await jalankanAman(bacaRuangA);
const hasilB = await jalankanAman(bacaRuangB);

console.log(hasilA);
console.log(hasilB);
text Keluaran
{ ok: false, error: 'sensor ruang A offline' }
{ ok: true, nilai: 25 }

Sensor ruang A melempar objek Error, dan pesannya dibaca pada blok catch melalui pempersempitan instanceof Error. Sensor ruang B tidak melempar apa pun, sehingga hasilnya berupa bentuk ok yang membawa angka. Kode yang memanggil jalankanAman untuk kedua sensor memperoleh tipe yang sama, yaitu Result<number, string>, sehingga kegagalan tidak dapat dilupakan karena cabangnya wajib ditangani sebelum nilai diakses.

Kesalahan umum#

Kesalahan pertama adalah menganggap nilai catch selalu berupa Error. Penegasan seperti (e as Error).message lolos kompilasi, tetapi menghasilkan undefined ketika nilai lemparannya berupa string. Pempersempitan dengan instanceof Error dan typeof memberikan hasil yang benar untuk semua bentuk nilai.

Kesalahan kedua adalah mencampur dua konvensi pada satu batas modul, yaitu sebagian fungsi melempar dan sebagian mengembalikan Result. Pemanggilnya tidak dapat mengetahui konvensi masing-masing fungsi tanpa membaca implementasinya, sehingga jalur kegagalannya mudah terlewat. Satu batas modul sebaiknya memilih satu konvensi, dan pembungkus seperti jalankanAman menjadi cara yang konsisten untuk menjembatani keduanya.

Kesalahan ketiga adalah membaca nilai sebelum memeriksa ok. Compiler menolaknya karena bidang nilai tidak ada pada bentuk kegagalan, sebagaimana dibuktikan berkas berikut.

ts baca-mentah.ts
// tidak-dijalankan
type Result<T, E> = { ok: true; nilai: T } | { ok: false; error: E };

function parseNilai(): Result<number, string> {
  return { ok: false, error: "bukan angka" };
}

const hasil = parseNilai();

console.log(hasil.nilai);
bash
# membuktikan pesan error untuk pembacaan nilai tanpa memeriksa ok
tsc --noEmit --pretty false baca-mentah.ts
text Keluaran
baca-mentah.ts(10,19): error TS2339: Property 'nilai' does not exist on type 'Result<number, string>'.
  Property 'nilai' does not exist on type '{ ok: false; error: string; }'.

Ringkasan#

  • Variabel catch bertipe unknown, sehingga nilai lemparan wajib dipersempit dengan instanceof Error sebelum message dibaca.
  • Perintah throw menerima nilai apa pun, dan tipenya tidak pernah dijanjikan oleh compiler.
  • Pola Result<T, E> menyatakan kegagalan sebagai union diskriminatif, sehingga cabang keberhasilan dan kegagalan diperiksa sebelum bidangnya diakses.
  • Pembungkus asinkron seperti jalankanAman mengubah fungsi yang melempar menjadi fungsi yang mengembalikan Result.

Referensi#