Elixir mengenal dua jalur penanganan error yang saling melengkapi, yaitu exception yang dilempar dengan raise dan nilai kegagalan berbentuk {:error, alasan}. Topik ini membahas try, rescue, dan after untuk menangkap exception, exception kustom dengan defexception, peran throw dan catch yang jarang digunakan, serta pertimbangan memilih di antara kedua jalur tersebut. Validasi nilai ujian digunakan sebagai contoh sepanjang topik agar kedua jalur dapat dibandingkan pada masalah yang sama.
Menangkap exception dengan try dan rescue#
raise/1 menghentikan eksekusi seketika dengan sebuah exception, yaitu struct yang membawa jenis dan pesan kegagalan. Blok try dan rescue menangkap exception itu sebelum process berhenti. Klausa pada rescue menyaring jenis exceptionnya, misalnya e in KeyError yang hanya menangkap KeyError, sedangkan exception jenis lain melewati blok ini dan merambat seperti biasa. Penyaringan per jenis menjaga blok rescue tetap sempit, karena hanya kegagalan yang memang diantisipasi yang tertangkap.
Jenis exception yang beredar pun beragam, seperti KeyError untuk kunci map yang tidak ada, ArithmeticError untuk perhitungan yang tidak sah, dan RuntimeError untuk kegagalan umum hasil raise "pesan". Setiap jenisnya berupa struct dengan modulnya sendiri, sehingga pencocokan e in KeyError pada klausa rescue sebenarnya adalah pencocokan pola struct, sama seperti yang dibahas pada topik map dan struct.
Contoh berikut mengambil nilai dari map siswa menggunakan notasi titik. Kunci :citra tidak ada pada map tersebut, sehingga aksesnya menimbulkan KeyError yang langsung ditangkap:
nilai = %{ali: 90, budi: 76}
try do
IO.puts("nilai citra: #{nilai.citra}")
rescue
e in KeyError -> IO.puts("KeyError tertangkap: #{Exception.message(e)}")
end
IO.puts("program tetap berjalan setelah rescue")KeyError tertangkap: key :citra not found in:
%{ali: 90, budi: 76}
program tetap berjalan setelah rescuePesan yang dicetak menempati beberapa baris karena KeyError pada Elixir 1.20 membentangkan isi map pada baris tersendiri yang berindentasi empat spasi. Hal yang penting dari keluaran itu adalah alurnya, yaitu exception yang tertangkap, pesan yang terbaca melalui Exception.message/1, dan eksekusi yang berlanjut ke pernyataan setelah blok try.
after: pembersihan yang selalu berjalan#
Klausa after melengkapi blok try dengan kode pembersihan yang dijalankan pada setiap jalur keluar, baik isi try selesai dengan sukses, exception tertangkap, maupun exception lolos dari semua klausa rescue. Tempat yang wajar bagi kode semacam ini adalah pekerjaan yang harus selalu diselesaikan, misalnya menutup koneksi atau menghapus berkas sementara. Urutan eksekusinya saat exception tertangkap pun dapat dinyatakan dengan pasti, yaitu klausa rescue dievaluasi lebih dahulu, kemudian after dijalankan:
try do
IO.puts("sebelum")
raise "simulasi kegagalan"
IO.puts("tidak pernah dicetak")
rescue
e in RuntimeError -> IO.puts("tangkap: #{Exception.message(e)}")
after
IO.puts("bersih")
endsebelum
tangkap: simulasi kegagalan
bersihKeluaran menegaskan urutan yang dinyatakan di atas. Baris sebelum berasal dari awal isi try, baris tangkap dari klausa rescue yang menangkap RuntimeError, dan baris bersih dari after yang berjalan paling akhir sebelum blok try selesai. Baris tidak pernah dicetak tidak muncul karena pernyataan itu berada setelah raise. Apabila tidak ada klausa rescue yang cocok, after tetap berjalan terlebih dahulu sebelum exception merambat keluar. Klausa ini tidak mengubah nilai akhir, yang tetap berasal dari isi try yang berhasil atau dari klausa rescue yang menangkap exception. Pembersihan sebaiknya juga dijaga sederhana, karena exception yang timbul di dalam after dapat menutupi exception aslinya.
Exception kustom dengan defexception#
defexception mendefinisikan struct exception milik modul sendiri, lengkap dengan callback message/1 yang merangkai pesannya. Exception semacam ini dilempar dengan raise NamaException, kunci: nilai, dan field yang diperlukan untuk menyusun pesan cukup dideklarasikan pada daftar fieldnya. Fungsi exception/1 yang mengubah argumen raise menjadi struct sudah disediakan oleh defexception, sehingga yang perlu dirangkai sendiri hanya pesannya. Struktur ini membuat exception tetap berupa data biasa, yaitu struct yang dapat dicocokkan, disimpan, atau dikirim antarprocess seperti nilai lain. Pesan yang dirangkai sendiri jauh lebih informatif daripada pesan generik karena isinya dapat menyebut nilai yang menyebabkan kegagalan.
Skrip utuh berikut menggabungkan kedua jalur pada satu validasi nilai ujian. Fungsi uraian/1 mengembalikan nilai untuk setiap kegagalan yang diantisipasi, sedangkan fungsi simpan!/1 menerjemahkan kegagalan itu menjadi exception kustom:
# validasi.exs — dua jalur penanganan error pada validasi nilai ujian
defmodule NilaiTidakValid do
defexception [:nilai]
def message(exception) do
"nilai #{inspect(exception.nilai)} di luar jangkauan 0 sampai 100"
end
end
defmodule NilaiUjian do
# jalur utama: kegagalan yang diantisipasi dikembalikan sebagai nilai
def uraian(nilai) when is_integer(nilai) and nilai in 0..100 do
{:ok, nilai}
end
def uraian(nilai) when is_integer(nilai) do
{:error, :di_luar_jangkauan}
end
def uraian(nilai_lain) do
{:error, {:bukan_bilangan, nilai_lain}}
end
# varian tegas: kegagalan diterjemahkan menjadi exception
def simpan!(nilai) do
case uraian(nilai) do
{:ok, nilai_baik} ->
IO.puts("nilai #{nilai_baik} tersimpan")
{:error, _alasan} ->
raise NilaiTidakValid, nilai: nilai
end
end
end
IO.inspect(NilaiUjian.uraian(80), label: "uraian 80")
IO.inspect(NilaiUjian.uraian(150), label: "uraian 150")
IO.inspect(NilaiUjian.uraian("80"), label: "uraian \"80\"")
NilaiUjian.simpan!(80)
try do
NilaiUjian.simpan!(150)
rescue
e in NilaiTidakValid -> IO.puts("tertangkap: #{Exception.message(e)}")
enduraian 80: {:ok, 80}
uraian 150: {:error, :di_luar_jangkauan}
uraian "80": {:error, {:bukan_bilangan, "80"}}
nilai 80 tersimpan
tertangkap: nilai 150 di luar jangkauan 0 sampai 100Skrip dapat dijalankan langsung setelah berkasnya disimpan:
# menjalankan skrip validasi
elixir validasi.exsuraian 80: {:ok, 80}
uraian 150: {:error, :di_luar_jangkauan}
uraian "80": {:error, {:bukan_bilangan, "80"}}
nilai 80 tersimpan
tertangkap: nilai 150 di luar jangkauan 0 sampai 100Keluaran memperlihatkan kedua jalur berdampingan. Tiga pemanggilan uraian/1 mengembalikan data kegagalan yang berbeda sesuai penyebabnya, satu pemanggilan simpan!/1 berhasil tanpa exception, dan pemanggilan simpan!/1 dengan nilai 150 berakhir pada pesan kustom hasil message/1. Konvensinya, nama fungsi yang diakhiri tanda seru, seperti simpan!/1, menunjukkan versi yang melempar exception, sedangkan versi tanpa tanda seru mengembalikan nilai.
throw dan catch#
throw mengeluarkan satu nilai dari sarang eksekusi yang dalam, dan nilai itu ditangkap oleh pasangan catch yang membungkus perhitungannya. Mekanisme ini bukan exception dan bukan bagian penanganan error sehari-hari, melainkan pintu keluar dini (early exit) untuk perhitungan yang hasilnya sudah diketahui di tengah jalan. Nilai yang dilempar dapat berupa data apa pun, dan pencocokannya dilakukan pada klausa catch beserta jenis lemparannya:
hasil =
try do
Enum.each(1..10_000, fn angka ->
if angka * angka > 20, do: throw({:melebihi, angka})
end)
:tidak_ditemukan
catch
:throw, {:melebihi, angka} -> {:melebihi_di, angka}
end
IO.inspect(hasil){:melebihi_di, 5}Keluaran {:melebihi_di, 5} menunjukkan perulangan berhenti tepat pada angka pertama yang kuadratnya melampaui 20, tanpa menelusuri seluruh deret. Hasil yang sama sebenarnya dapat diperoleh dengan fungsi pengulangan yang mendukung penghentian dini seperti Enum.reduce_while/3, sehingga throw memang bukan kebutuhan harian. Apabila tidak ada catch yang menangkap, lemparan berakhir sebagai error yang menghentikan process, sehingga lemparan tidak pernah boleh dilepas tanpa penangkapnya. Kedua jalur lain, yaitu nilai {:error, alasan} dan exception dengan raise, hampir selalu mencukupi untuk aplikasi, dan throw terutama layak dikenali agar kode pustaka yang menggunakannya tetap terbaca.
Memilih antara raise dan nilai error#
Pemilihan antara kedua jalur ditentukan oleh satu pertimbangan utama, yaitu apakah kegagalan itu jawaban yang wajar atas masukan, atau tanda bahwa asumsi kode sudah runtuh. Tabel berikut merangkum pertimbangannya:
| Aspek | Nilai {:error, alasan} | Exception dengan raise |
|---|---|---|
| Sifat kegagalan | Diantisipasi, bagian dari alur normal | Tidak diantisipasi, bug atau kondisi luar biasa |
| Cara diterima pemanggil | Dicocokkan lewat case atau with | Ditangkap dengan try/rescue atau dibiarkan merambat |
| Keterlihatan pada kontrak fungsi | Jelas pada nilai kembalian | Tidak terlihat sampai kejadian |
| Akibat apabila diabaikan | Pemanggil tidak dapat melanjutkan tanpa pencocokan | Process berhenti, lalu supervisor mengambil alih |
| Contoh | Input yang salah, data tidak ditemukan | Pembagian dengan nol, kunci map yang tidak ada |
Contoh validasi pada topik ini menggambarkan pembagian perannya. Fungsi uraian/1 berada pada jalur pertama karena nilai yang tidak memenuhi syarat adalah jawaban yang wajar atas masukan. Fungsi simpan!/1 menggunakan exception karena pemanggilnya menuntut keberhasilan, dan kegagalannya berarti kontrak fungsi dilanggar. Sikap yang sama sebaiknya dipertahankan secara konsisten di dalam satu modul, sehingga pemanggil tahu apa yang harus diharapkan dari setiap fungsinya.
Ringkasan#
trydanrescuemenangkap exception per jenisnya, dan pesannya dibaca melaluiException.message/1, bukan melalui field:message.afterberjalan pada setiap jalur keluar, dievaluasi setelah klausa rescue, dan tetap berjalan apabila exception merambat keluar.defexceptionmembuat exception kustom denganmessage/1yang dilempar melaluiraise NamaException, kunci: nilai.- Nilai
{:error, alasan}untuk kegagalan yang diantisipasi danraiseuntuk pelanggaran asumsi, sedangkanthrowdancatchjarang digunakan di luar pustaka.
Referensi#
- Modul Kernel — Hexdocs Elixir
— dokumentasi
raise,try,rescue,catch,after, dandefexception. - Modul Exception — Hexdocs Elixir
— kontrak struct exception beserta fungsi
Exception.message/1. - Error Handling — Erlang Reference Manual
— semantik
throwdancatchpada level Erlang.