Topik ini membahas posisi exception pada kode F#, yaitu situasi ketika melempar exception masih menjadi pilihan yang tepat dan cara menangkapnya dengan try-with. Pembahasan mencakup batas antara kegagalan domain dan kegagalan sistem, failwith, exception khusus, try-finally untuk pembersihan sumber daya, serta melempar ulang dengan reraise().
Ketika exception masih tepat digunakan#
Result dan option menyelesaikan kegagalan yang wajar dan terduga pada alur domain, tetapi itu bukan berarti exception harus dihindari sepenuhnya. Ada keadaan ketika melempar exception justru pilihan yang jujur, yaitu ketika program tidak memiliki cara yang bermakna untuk melanjutkan eksekusinya. Kelompok keadaan tersebut antara lain sebagai berikut.
- Kegagalan sistemik, misalnya memori habis, disk penuh, atau jaringan terputus di tengah proses. Tidak ada nilai Result yang bermakna karena seluruh alur tidak dapat dilanjutkan.
- Kesalahan pemanggil yang merupakan bug, misalnya indeks di luar batas atau argumen yang menyalahi kontrak fungsi. Kegagalan ini seharusnya ditemukan saat pengembangan, bukan ditangani sebagai data.
- Pustaka .NET yang memang melempar exception, karena API tersebut tidak dapat diubah dan pemanggil F# perlu menangkapnya.
Ketiga kelompok tersebut memiliki ciri yang sama, yaitu kegagalannya berada di luar alur data yang dimodelkan. Menjadikannya nilai Result hanya memindahkan tanggung jawab ke tempat yang tidak dapat melakukan apa pun terhadap penyebabnya. Bagian berikut memperlihatkan cara menangkap exception yang tetap muncul dari sumber-sumber tersebut.
Menangkap exception dengan try-with#
Bentuk try-with menempatkan kode yang mungkin melempar di antara try dan with, lalu mencocokkan exception yang tertangkap dengan pola seperti pada match. Pola :? TipeException as e menangkap exception bertipe tertentu dan mengikat objeknya ke nama e, sedangkan | e -> menangkap semua exception. Blok with adalah ekspresi, sehingga hasilnya dapat diikat atau dikembalikan seperti ekspresi lain.
// try-with menangkap exception yang dilempar pustaka .NET
try
printfn "terbaca: %d" (System.Int32.Parse "42")
with
| :? System.FormatException as e ->
printfn "gagal mengurai: %s" e.Message
try
printfn "terbaca: %d" (System.Int32.Parse "tujuh")
with
| :? System.FormatException as e ->
printfn "gagal mengurai: %s" e.Messageterbaca: 42
gagal mengurai: The input string 'tujuh' was not in a correct format.Panggilan pertama berhasil tanpa menyentuh blok with, sedangkan panggilan kedua melempar FormatException dan tertangkap dengan pesan aslinya. Cabang yang tidak cocok dengan tipe exception mana pun membuat exception berlanjut ke atas, yaitu menghentikan program apabila tidak ada handler lain yang menangkapnya. Penangkapan yang terlalu luas menyembunyikan bug yang seharusnya terlihat, sehingga daftar tipe exception yang ditangkap sebaiknya dibatasi sesempit mungkin.
Pustaka .NET yang melempar dapat dibungkus sekali di batas, sehingga sisanya menggunakan Result secara konsisten. Fungsi uraiAman berikut menempatkan pemanggilan Int32.Parse di dalam try dan mengubah kegagalannya menjadi Error. Pesan pada e.Message digunakan langsung sebagai alasan kegagalan, dan informasi aslinya tidak hilang.
let uraiAman (teks: string) : Result<int, string> =
try
Ok (System.Int32.Parse teks)
with
| :? System.FormatException as e -> Error e.Message
printfn "%A" (uraiAman "42")
printfn "%A" (uraiAman "dua")Ok 42
Error "The input string 'dua' was not in a correct format."failwith dan exception khusus#
Fungsi failwith melempar System.Exception dengan pesan yang diberikan, dan penulisannya paling ringkas di antara pilihan yang tersedia. Kegunaannya ada pada skrip dan prototipe, yaitu ketika kegagalan ditandai sementara agar tidak dilupakan. Pada kode domain yang matang, System.Exception yang tidak berstruktur kurang bermanfaat, karena pemanggil tidak dapat membedakannya dari exception lain tanpa membaca pesannya.
Kebutuhan yang lebih terstruktur dipenuhi oleh exception khusus. Deklarasinya menyerupai discriminated union dengan kata kunci exception, dan kasusnya boleh membawa data. Melemparnya menggunakan fungsi raise, sedangkan penangkapannya menggunakan pola nama kasus di dalam with.
// failwith untuk penandaan cepat, exception khusus untuk kegagalan domain
try
failwith "data belum lengkap"
with
| e -> printfn "tertangkap: %s" e.Message
exception HariPenuh of hari: string
let terisi = [ "Senin"; "Rabu" ]
let agendakan (hari: string) : string =
if List.contains hari terisi then
raise (HariPenuh hari)
else
sprintf "piket %s terjadwal" hari
try
printfn "%s" (agendakan "Selasa")
printfn "%s" (agendakan "Rabu")
with
| HariPenuh hari -> printfn "gagal: %s sudah terisi piket" haritertangkap: data belum lengkap
piket Selasa terjadwal
gagal: Rabu sudah terisi piketKasus HariPenuh hari membawa hari yang bentrok, sehingga handler menerima data yang relevan tanpa mengurai pesan teks. Jenis exception yang berbeda dapat dideklarasikan untuk kegagalan yang berbeda, dan pemilihan cabang pada with menjadi tepat karena pencocokannya berdasarkan tipe, bukan berdasarkan isi pesan.
Pembersihan dengan try-finally#
Bentuk try-finally menempatkan kode pembersihan di antara finally dan akhir blok, dan blok tersebut dijalankan pada jalur sukses maupun jalur exception. Kegunaannya ada pada sumber daya yang harus dilepas, misalnya koneksi, berkas yang terbuka, atau sensor yang dimatikan. Kode pembersihan tidak menelan exception yang sedang berlangsung, melainkan hanya menambahkan langkah penutup sebelum exception berlanjut.
// try-finally menjamin pembersihan pada jalur sukses maupun jalur gagal
let jalankanSensor (gangguan: bool) =
let mutable sensor = "nonaktif"
try
sensor <- "aktif"
printfn "sensor: %s" sensor
if gangguan then failwith "pembacaan sensor terganggu"
printfn "pembacaan: 27.5 derajat"
finally
sensor <- "nonaktif"
printfn "sensor: %s" sensor
try
jalankanSensor false
with
| e -> printfn "tertangkap: %s" e.Message
try
jalankanSensor true
with
| e -> printfn "tertangkap: %s" e.Messagesensor: aktif
pembacaan: 27.5 derajat
sensor: nonaktif
sensor: aktif
sensor: nonaktif
tertangkap: pembacaan sensor tergangguPanggilan pertama menuntaskan seluruh badan try, sehingga sensor dimatikan setelah pembacaan dicetak. Panggilan kedua terganggu di tengah badan try, tetapi blok finally tetap dijalankan sebelum exception ditangkap di luar. Urutan pada keluaran memperlihatkannya, yaitu status sensor nonaktif tercetak sebelum pesan tertangkap muncul.
Melempar ulang dengan reraise()#
Kadang exception perlu dicatat oleh lapisan tertentu, tetapi tetap diteruskan ke pemanggilnya. Fungsi reraise() melakukan hal itu di dalam blok with, yaitu melempar kembali exception yang sedang ditangkap tanpa membungkusnya dalam exception baru. Jejak aslinya dipertahankan, sehingga letak kegagalan yang pertama tetap terlihat.
// reraise() melempar kembali exception yang sedang ditangkap
let bacaPembacaan (teks: string) =
try
System.Int32.Parse teks
with
| e ->
printfn "pencatat: pembacaan %s ditolak (%s)" teks e.Message
reraise()
try
printfn "terbaca: %d" (bacaPembacaan "42")
printfn "terbaca: %d" (bacaPembacaan "dua")
with
| e -> printfn "pelapis: %s" e.Messageterbaca: 42
pencatat: pembacaan dua ditolak (The input string 'dua' was not in a correct format.)
pelapis: The input string 'dua' was not in a correct format.Handler dalam mencatat kegagalan, lalu reraise() mengembalikan FormatException yang sama ke handler luar. Melempar ulang dengan raise e bukan penggantinya, karena panggilan itu menambahkan posisi lemparan baru pada jejak exception, sedangkan reraise() membiarkan jejaknya apa adanya. Kombinasi pencatatan dan reraise() lazim pada lapisan yang perlu mengamati kegagalan tanpa berhak memutusnya.
Batas jujur antara Result dan exception#
Pembagian yang jujur menempatkan kegagalan pada dua kategori. Kegagalan domain adalah bagian dari alur yang wajar, misalnya data tidak ditemukan, masukan tidak sah, atau batas terlampaui, dan kelompok ini dibawa dengan Result atau option. Kegagalan sistem berada di luar model, misalnya memori habis atau pustaka .NET yang melempar tanpa alternatif, dan kelompok ini ditangani dengan try-with pada batas yang tepat.
| Ciri | Kegagalan domain | Kegagalan sistem |
|---|---|---|
| Kemunculan | terduga dan wajar | jarang dan tidak terduga |
| Pembawa | Result atau option | exception .NET |
| Penanganan | nilai yang diproses pemanggil | try-with pada batas lapisan |
| Respons yang wajar | pesan kepada pengguna | hentikan proses, atau catat lalu lempar ulang |
Aturan praktisnya, fungsi yang kegagalannya dapat dipahami dan diantisipasi pemanggil mengembalikan Result, sedangkan fungsi yang kegagalannya berarti kontrak sudah dilanggar boleh melempar. Pembungkusan try-with ditempatkan pada batas lapisan, misalnya di sekitar pemanggilan pustaka .NET atau di tepi aplikasi, bukan tersebar di setiap fungsi. Dengan pembagian ini, tipe hasil menceritakan alur yang wajar, sedangkan exception melaporkan kejadian yang tidak wajar.
Ringkasan#
- Exception masih tepat untuk kegagalan sistemik, bug pemanggil, dan pustaka .NET yang memang melempar.
- try-with mencocokkan tipe exception seperti
matchdan merupakan ekspresi, sehingga hasilnya dapat diikat atau dibungkus menjadi Result. - failwith melempar System.Exception untuk penandaan cepat, sedangkan exception khusus dengan kata kunci
exceptionmembawa data yang dapat dicocokkan. - try-finally menjamin kode pembersihan pada jalur sukses maupun gagal, dan reraise() melempar ulang exception yang sedang ditangkap dengan jejaknya utuh.
Referensi#
- Exception handling — Microsoft Learn — Indeks dokumentasi penanganan exception di F#.
- The try…with expression — Microsoft Learn — Dokumentasi try-with beserta pola penangkapannya.
- Exception types — Microsoft Learn — Deklarasi exception khusus dan fungsi raise.
- The try…finally expression — Microsoft Learn — Dokumentasi try-finally untuk pembersihan sumber daya.