Belajar Haskell - Error Handling & Exception
Episode 16 of 23

Belajar Haskell - Error Handling & Exception

Menangani kegagalan secara terstruktur di Haskell: error yang recoverable lewat Maybe dan Either dengan ExceptT dan MonadError, exception I/O dengan Control.Exception dan bracket untuk resource safety, serta mengganti fungsi partial seperti head dan fromJust dengan fungsi total yang aman.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Tidak ada program yang selalu berhasil. Di episode 16 ini kita merapikan cara Haskell menangani kegagalan — dan pentingnya bab ini lebih besar dari kelihatannya, karena pilihan arsitektur error menentukan kualitas API yang kalian bangun. Di bahasa lain, hampir semua kegagalan diperlakukan sebagai exception yang bisa "meledak" di mana saja; di Haskell, kegagalan adalah nilai yang bisa diperiksa, dikomposisikan, dan dipetakan ke response HTTP.

Mengapa bab ini penting? Karena perbedaan antara "aplikasi yang tidak pernah crash tak terduga" dan "aplikasi yang sering 500" sering kali hanya soal disiplin ini: error recoverable ditangani sebagai data, exception hanya untuk hal langka dan tak terduga.

Dua Kelas Kegagalan

Haskell membedakan dua jenis kegagalan secara eksplisit:

JenisContohAlat
Error recoverableinput invalid, data tidak ditemukanMaybe, Either, ExceptT
Exceptionfile tidak ada, koneksi putusControl.Exception

Aturannya: gunakan nilai error (Maybe/Either) untuk kegagalan yang diharapkan dan harus diproses kode; gunakan exception untuk kegagalan tak terduga dari lingkungan (I/O, resource).

Maybe dan Either: Error sebagai Nilai

Dasar-dasarnya sudah kita bahas di episode 11. Yang perlu ditambahkan di sini: Either dengan tipe error yang terstruktur. Alih-alih Either String, gunakan ADT:

Tipe error terstruktur
data AppError
    = ValidationError String
    | NotFoundError String
    | DatabaseError String
    deriving (Show)
 
simpanUser :: String -> Either AppError User
simpanUser nama
    | null nama  = Left (ValidationError "nama tidak boleh kosong")
    | otherwise  = Right (User 1 nama)

Dengan ADT, AppError bisa dicocokkan dengan pola, di-map ke status HTTP, dan di-log secara konsisten:

Map error ke status HTTP
httpStatus :: AppError -> Int
httpStatus (ValidationError _) = 400
httpStatus (NotFoundError _)   = 404
httpStatus (DatabaseError _)   = 500

Sekarang seluruh aplikasi punya satu bahasa error — bukan string acak yang tersebar.

ExceptT dan MonadError: Error di Dalam Stack Monad

Saat handler berurutan memakai IO (database, HTTP), error perlu berjalan di dalam stack monad. ExceptT e IO a menyatukan keduanya:

ExceptT di atas IO
import Control.Monad.Except
 
type AppM a = ExceptT AppError IO a
 
cariUser :: Int -> AppM User
cariUser id = do
    hasil <- liftIO (queryDB id)          -- IO murni diangkat ke stack
    case hasil of
        Nothing -> throwError (NotFoundError ("user " ++ show id))
        Just u  -> pure u
 
getUserHandler :: Int -> AppM User
getUserHandler id = do
    u <- cariUser id
    pure u

throwError membawa error ke Left; liftIO membungkus aksi IO ke dalam stack. Di puncak stack, jalankan dan tangkap hasilnya:

Menjalankan ExceptT
runApp :: AppM a -> IO (Either AppError a)
runApp = runExceptT

MonadError adalah typeclass yang mengabstraksi kemampuan "lempar error" ini — kode yang memakai MonadError e m bisa dijalankan di atas ExceptT, atau diganti implementasinya untuk test.

Tip

Jika kalian pernah memakai try/catch secara manual di bahasa lain, pola Haskell adalah kebalikannya: jangan lempar exception untuk error yang bisa diprediksi. Kembalikan Left/Nothing sebagai data, dan serahkan "penerjemahan" (ke status HTTP, ke log) ke lapisan terluar.

Control.Exception: Exception untuk I/O

Untuk kegagalan lingkungan, Control.Exception menyediakan mekanisme seperti try/catch. Contoh membaca file yang mungkin tidak ada:

Menangkap exception I/O
import Control.Exception (try, IOException)
 
bacaAman :: FilePath -> IO (Either IOException String)
bacaAman fp = try (readFile fp)

try mengembalikan Left exception atau Right nilai. Untuk resource safety — memastikan resource ditutup apa pun yang terjadi — gunakan bracket:

bracket - resource selalu ditutup
import Control.Exception (bracket)
import qualified Data.Text.IO as T
 
prosesFile :: FilePath -> IO ()
prosesFile fp = bracket
    (openFile fp ReadMode)      -- akuisisi resource
    hClose                      -- release: dijalankan SELALU
    (\h -> T.hGetContents h >>= T.putStrLn)

bracket dijamin memanggil hClose baik aksi sukses maupun melempar exception — inilah idiom withResource/try-with-resources ala Haskell, dan implementasi internal dari withResource di episode 15.

Menghindari Fungsi Partial

Fungsi partial adalah fungsi yang crash untuk sebagian input: head [], fromJust Nothing, tail [], read "abc". Ini adalah sumber crash runtime yang paling mudah dihindari.

Partial vs total
-- PARTIAL (berbahaya): crash saat list kosong
pertama :: [a] -> a
pertama (x:xs) = x
-- pertami []  ->  error
 
-- TOTAL (aman): selalu menghasilkan nilai
pertamaSafely :: [a] -> Maybe a
pertamaSafely []    = Nothing
pertamaSafely (x:_) = Just x

Cara menghindarinya:

  • headsafeHead :: [a] -> Maybe a atau pakai Data.Maybe.listToMaybe.
  • fromJustpattern matching Just x <- ... atau maybe def f:
  • !! (index) → lookup atau Data.List.findIndex dengan Maybe.
Praktik aman
import Data.Maybe (maybe)
 
sapaPengguna :: Maybe String -> String
sapaPengguna = maybe "tamu" (\n -> "Halo " ++ n)

maybe menerapkan fungsi ke nilai dalam Just, atau mengembalikan default untuk Nothing — tanpa pernah crash.

Warning

Menggunakan head/fromJust adalah bau kode di Haskell modern. GHC bahkan punya warning -Wx-partial yang menandai fungsi-fungsi partial dari Prelude. Aktifkan warning ini (ghc-options: -Wall -Wx-partial) dan jadikan kegagalan tak terduga mustahil — itulah esensi fungsi total (dibahas lanjut di episode 17).

MonadFail vs Exception: Tempatnya Berbeda

Pola Just x <- ... (episode 11) memakai MonadFail — kegagalan terstruktur di dalam monad (misal Nothing untuk Maybe). Exception dipakai untuk hal yang tak terduga. Jangan campur keduanya: jika kode bisa menghasilkan Nothing/Left sebagai bagian dari domain, selalu tangani sebagai data.

Kesalahan Umum (Common Pitfalls)

  1. Exception untuk error yang bisa diprediksi — input user yang invalid bukan exception; itu nilai Left/Nothing.
  2. head/fromJust/tail tanpa jaminan — ganti dengan versi total; aktifkan -Wx-partial.
  3. Lupa bracket — resource bocor saat exception; selalu wrapping resource dengan bracket.
  4. throwError di kode yang seharusnya pureExceptT punya versi pure (Except), gunakan sesuai konteks.
  5. Menangkap SomeException — menangkap semua exception menyembunyikan bug; tangkap tipe spesifik (IOException, dll).

Penutup

Inti yang harus dibawa pulang:

  • Error recoverable = nilai (Maybe/Either); exception = kegagalan lingkungan tak terduga.
  • Gunakan ADT untuk tipe error; petakan ke status HTTP dengan satu fungsi.
  • ExceptT + MonadError membawa error ke dalam stack monad yang melibatkan IO.
  • bracket menjamin resource ditutup apa pun yang terjadi.
  • Fungsi partial (head, fromJust) adalah musuh; tulis fungsi total yang selalu menghasilkan nilai.

Di episode 17 selanjutnya kita akan membahas keamanan & best practice — total functions dan type-driven design untuk membuat illegal states tidak representable, menghindari unsafe*, validasi input dengan Megaparsec, serta mengaudit dependensi dengan cabal-audit dan CVE. Sampai jumpa di episode 17!

Belajar Haskell - Error Handling & Exception | Belajar Haskell