Exception secara jujur

Situasi ketika exception .NET masih sah diterapkan dan cara menanganinya dengan try-with.

Daftar isi

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.

fsharp urai-format.fsx
// 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.Message
text Keluaran
terbaca: 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.

fsharp Contoh: membungkus pustaka .NET yang melempar menjadi Result
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")
text Keluaran
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.

fsharp pengecualian-khusus.fsx
// 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" hari
text Keluaran
tertangkap: data belum lengkap
piket Selasa terjadwal
gagal: Rabu sudah terisi piket

Kasus 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.

fsharp bersihkan-sensor.fsx
// 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.Message
text Keluaran
sensor: aktif
pembacaan: 27.5 derajat
sensor: nonaktif
sensor: aktif
sensor: nonaktif
tertangkap: pembacaan sensor terganggu

Panggilan 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.

fsharp lempar-ulang.fsx
// 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.Message
text Keluaran
terbaca: 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.

CiriKegagalan domainKegagalan sistem
Kemunculanterduga dan wajarjarang dan tidak terduga
PembawaResult atau optionexception .NET
Penanganannilai yang diproses pemanggiltry-with pada batas lapisan
Respons yang wajarpesan kepada penggunahentikan 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 match dan merupakan ekspresi, sehingga hasilnya dapat diikat atau dibungkus menjadi Result.
  • failwith melempar System.Exception untuk penandaan cepat, sedangkan exception khusus dengan kata kunci exception membawa 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#