Belajar Scala - Error Handling & Validation
Episode 16 of 23

Belajar Scala - Error Handling & Validation

Merancang penanganan error yang eksplisit dan aman: Option, Either, dan Try untuk operasi yang bisa gagal, error effect ala ZIO Cause, lalu validation yang mengumpulkan semua kesalahan dengan cats Validated dan ADT domain error.

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

Pendahuluan

Semua program bisa gagal — file tidak ada, koneksi terputus, input tidak valid. Perbedaan antara aplikasi yang rapuh dan yang tangguh bukan pada apakah ia gagal, melainkan bagaimana kegagalan direpresentasikan dan ditangani. Inilah topik episode ini: error handling dan validation di Scala.

Mengapa penting? Karena Scala memberi kalian alat untuk membuat error eksplisit di tipe — tidak mengintai di null atau exception yang tak terduga. Setelah episode ini, kalian akan merancang API yang "tidak mungkin" salah pakai: tipe itu sendiri yang memandu penanganan semua kemungkinan kegagalan.

Tiga Container untuk Operasi yang Bisa Gagal

Kita sudah mengenal Option (episode 5) dan Either (episode 7). Sekarang lengkap dengan Try:

ContainerMaknaKapan dipakai
Option[A]Ada (Some) atau tidak ada (None)Nilai yang bisa absen — misal lookup
Either[L, R]Gagal (Left) atau sukses (Right)Hasil dengan dua kemungkinan berlabel
Try[A]Sukses (Success) atau exception (Failure)Membungkus kode yang melempar exception

Try: Membungkus Kode yang Melempar

Try membungkus exception
import scala.util.Try
 
def parseAmount(s: String): Try[Double] =
  Try(s.toDouble)
 
parseAmount("250000")       // Success(250000.0)
parseAmount("bukan-angka")  // Failure(NumberFormatException)

Try adalah jembatan antara dunia Java (yang suka melempar) dan dunia Scala (yang suka tipe). Ketiga container bisa di-map, di-flatMap, dan diproses dengan for:

Komposisi Try
val result: Try[Double] =
  for
    a <- parseAmount("100")
    b <- parseAmount("50")
  yield a + b

Representasi Error yang Jelas

Container saja tidak cukup; error-nya harus bisa dipahami. Jangan melempar String mentah — definisikan domain error sebagai ADT (pelajaran episode 7):

Domain error sebagai ADT
enum OrderError:
  case InvalidAmount(msg: String)
  case InsufficientStock(productId: Int)
  case CustomerNotFound(id: Int)
 
type Result[A] = Either[OrderError, A]

Dengan tipe ini, pemanggil tahu semua kegagalan yang mungkin terjadi — dan compiler memaksa mereka menanganinya:

Penanganan eksplisit
def processOrder(amount: Double): Result[String] =
  if amount <= 0 then Left(OrderError.InvalidAmount("jumlah harus positif"))
  else Right(s"pesanan diproses: Rp $amount")
 
processOrder(1000) match
  case Right(msg)  => println(msg)
  case Left(err)   => err match
    case OrderError.InvalidAmount(m) => println(s"input salah: $m")
    case _                            => println("kegagalan lain")

Error Handling di Effect System

Di effect system (episode 11), error bisa dimodelkan di level tipe maupun sebagai failure efek.

Cats-effect: Recover dan Handling

IO dengan handling
import cats.effect.IO
import cats.syntax.all.*
 
val program: IO[String] =
  IO.raiseError(new RuntimeException("gagal")).recover {
    case _: RuntimeException => "berhasil pulih"
  }

recover menangkap kegagalan dan memberikan nilai pengganti; attempt mengubah IO[A] menjadi IO[Either[Throwable, A]]:

attempt mengubah error jadi nilai
val safe: IO[Either[Throwable, Int]] =
  IO(1 / 0).attempt

ZIO: Cause di Level Tipe

ZIO membawa error di tipe (ZIO[R, E, A]) sehingga failure adalah bagian dari kontrak:

ZIO dengan error bertipe
import zio.*
 
enum AppError:
  case DbDown(msg: String)
  case NotFound
 
def loadUser(id: Int): ZIO[Any, AppError, String] =
  if id == 1 then ZIO.succeed("Sari")
  else ZIO.fail(AppError.NotFound)
 
val handled: ZIO[Any, Nothing, String] =
  loadUser(99).catchAll {
    case AppError.NotFound    => ZIO.succeed("fallback: tamu")
    case AppError.DbDown(msg) => ZIO.fail(new RuntimeException(msg))
  }

catchAll menangani semua AppError; yang tidak tertangani naik sebagai error bertipe. ZIO Cause adalah representasi internal kegagalan yang bisa memuat multiple failures dan interruptions — konsep lanjutan yang berguna saat debugging fiber.

Validation: Mengumpulkan Semua Kesalahan

Masalah dengan Either untuk validasi form: ia berhenti di error pertama. Saat user mengirim form dengan 5 field salah, kalian ingin menampilkan semua 5 kesalahan sekaligus. Solusinya adalah accumulating validation.

cats Validated

Validated dari cats mengumpulkan semua error (accumulation), bukan fail-fast:

Dependency cats
libraryDependencies += "org.typelevel" %% "cats-core" % "2.13.0"
Validated mengumpulkan error
import cats.data.Validated
import cats.syntax.all.*
 
case class UserInput(name: String, email: String, age: Int)
 
def checkName(name: String): Validated[String, String] =
  if name.nonEmpty then Validated.valid(name)
  else Validated.invalid("nama wajib diisi")
 
def checkAge(age: Int): Validated[String, Int] =
  if age >= 17 then Validated.valid(age)
  else Validated.invalid("harus 17 tahun atau lebih")
 
def validate(input: UserInput): Validated[String, UserInput] =
  (checkName(input.name), checkAge(input.age))
    .mapN((n, a) => input.copy(name = n, age = a))
 
val bad = validate(UserInput("", "a@b.c", 10))
// Invalid(nama wajib diisi, harus 17 tahun atau lebih) - dua error!
 
val ok = validate(UserInput("Sari", "a@b.c", 25))
// Valid(UserInput(Sari, a@b.c, 25))

mapN menggabungkan beberapa Validated — jika semua valid, jalankan fungsi; jika ada yang invalid, kumpulkan semuanya. .combine di List[Validated] melakukan hal yang sama untuk banyak field.

Note

Pilihannya penting dan sadar: fail-fast (Either/IO.raiseError) untuk alur yang menghentikan — misal otorisasi gagal — dan accumulating (Validated) untuk input batch — misal validasi form. Keduanya punya tempat; mengenali kapan memakai yang mana adalah keterampilan desain.

Validated dalam Effect System

Untuk memakai Validated di dalam program effect, cukup konversi di ujung:

Validated di dalam IO
import cats.data.Validated
import cats.effect.IO
 
val program: IO[Either[List[String], UserInput]] =
  IO.pure(validate(UserInput("", "a@b.c", 10)))
    .map(_.toEither)   // Validated -> Either
    .map(_.left.map(List(_)))

Common Pitfalls

  • Melempar exception untuk alur normal — gunakan Either/Option; exception untuk hal yang benar-benar luar biasa.
  • Error bertipe String — tanpa ADT, pemanggil tidak bisa membedakan kegagalan dan compiler tidak bisa membantu. Selalu definisikan domain error.
  • Option untuk error yang butuh pesanNone tidak membawa informasi; pakai Either[Error, A] bila butuh detail.
  • Validasi fail-fast saat user butuh semua error — gunakan Validated untuk form/batch.

Penutup

Inti yang harus dibawa pulang:

  • Option, Either, Try merepresentasikan operasi yang bisa gagal secara eksplisit di tipe.
  • Definisikan domain error sebagai ADT — bukan String mentah.
  • Di effect system: recover/attempt (cats-effect) dan error di level tipe (ZIO), dengan Cause untuk analisis mendalam.
  • Validated mengumpulkan semua kesalahan (accumulating) — lawan dari fail-fast.
  • Pilih fail-fast vs accumulating sesuai kebutuhan alur.

Di episode 17 selanjutnya, kita mengamankan aplikasi: keamanan dan best practice — immutability, menghindari null, input validation, secret handling, dan dependency scanning. Sampai jumpa di episode 17!

Belajar Scala - Error Handling & Validation | Belajar Scala