Swift 6.2 Typed Throws: Panduan Lengkap Error Handling Modern di iOS 26
Typed throws di Swift 6.2 memungkinkan fungsi mendeklarasikan tipe error spesifik di signature, sehingga compiler menjamin exhaustive catch tanpa downcast. Panduan lengkap dengan sintaks, perbandingan vs Result, migrasi bertahap, dan pola production di iOS 26.
Typed throws di Swift 6.2 adalah fitur error handling yang memungkinkan fungsi mendeklarasikan tipe error spesifik di signature-nya lewat sintaks throws(MyError), sehingga compiler bisa memastikan exhaustive catch dan menghilangkan kebutuhan downcast as MyError di sisi caller. Jujur, sejak Swift 1, saya sudah terbiasa dengan throws yang melempar any Error begitu saja (warisan filosofi Objective-C yang permisif). Di iOS 26 dengan Swift 6.2, kita akhirnya punya alternatif type-safe yang tetap kompatibel dengan kode lama, tanpa harus membungkus semuanya ke Result<Success, Failure>.
Sintaks baru func parse() throws(ParseError) -> Data mendeklarasikan tipe error spesifik di signature fungsi.
Compiler menjamin exhaustive catch. Block catch tahu persis tipe error yang ditangani, tanpa cast.
Typed throws bersifat opt-in. throws tanpa anotasi tetap berarti throws(any Error), jadi tidak ada breaking change.
throws(Never) setara dengan fungsi non-throwing dan dioptimasi compiler menjadi zero-cost.
Gunakan typed throws untuk API library yang error domain-nya tertutup. Hindari di entry point yang melempar banyak jenis error tidak terkait.
Typed throws tidak menggantikan Result. Keduanya punya use case berbeda, dan keduanya bisa hidup berdampingan.
Apa itu typed throws di Swift 6.2?
Typed throws adalah ekstensi mekanisme error handling Swift yang memperbolehkan fungsi menyatakan tipe error konkret yang bisa dilemparnya. Sebelum Swift 6, semua fungsi throws implisit melempar any Error, sebuah tipe existential yang menyembunyikan informasi error sampai caller melakukan pattern matching atau cast. Sejak SE-0413 matang di Swift 6.2 dan stabil di toolchain Xcode 26, kita bisa menulis throws(MyError) untuk membatasi error domain di level signature.
Perbedaan ini bukan kosmetik. Saat saya menulis library jaringan kecil tahun 2014 dengan Objective-C, semua kegagalan dibungkus NSError generik dengan userInfo dictionary. Caller tidak tahu apa yang mungkin gagal sampai membaca dokumentasi (kalau dokumennya ada). Swift 1 memperbaiki sebagian dengan throws, tapi tipe error tetap disembunyikan. Typed throws menutup celah terakhir: signature fungsi sekarang menjadi kontrak lengkap antara author dan caller. Untuk library Swift modern, ini berarti API yang self-documenting dan refactor-safe. Menambah varian error baru tidak lagi diam-diam memengaruhi konsumen.
Sintaks dasar dan contoh pertama
Sintaks typed throws mengikuti pola yang konsisten dengan deklarasi fungsi Swift. Tipe error ditulis dalam tanda kurung setelah keyword throws, sebelum return type. Berikut contoh minimum yang lengkap dan bisa langsung di-compile di Xcode 26.
enum ParseError: Error {
case emptyInput
case invalidNumber(String)
case outOfRange(Int)
}
func parseScore(_ input: String) throws(ParseError) -> Int {
guard !input.isEmpty else {
throw ParseError.emptyInput
}
guard let value = Int(input) else {
throw ParseError.invalidNumber(input)
}
guard (0...100).contains(value) else {
throw ParseError.outOfRange(value)
}
return value
}
do {
let score = try parseScore("87")
print("Skor: \(score)")
} catch .emptyInput {
print("Input kosong")
} catch .invalidNumber(let raw) {
print("Bukan angka: \(raw)")
} catch .outOfRange(let n) {
print("Di luar rentang: \(n)")
}
Perhatikan tiga hal. Pertama, signature throws(ParseError) mengikat fungsi pada satu tipe error. Kedua, di dalam block catch kita bisa menulis .emptyInput langsung tanpa qualifier ParseError, karena compiler tahu tipenya dari signature. Ketiga, ketiga arm catch di atas sudah exhaustive. Tidak perlu catch generik di akhir cuma untuk menenangkan compiler.
Cara kerja compiler dengan exhaustive catch
Exhaustive catch adalah selling point utama typed throws. Ketika tipe error adalah enum dengan jumlah case terbatas, compiler memeriksa apakah block do/catch menangani semua case persis seperti switch exhaustive. Tambahkan satu case baru ke ParseError, dan setiap call site yang melakukan pattern matching akan langsung memunculkan warning atau error compile. Refactor yang dulu rapuh sekarang dipandu kompiler.
// Setelah menambah case baru ke ParseError
enum ParseError: Error {
case emptyInput
case invalidNumber(String)
case outOfRange(Int)
case containsWhitespace // baru
}
do {
let score = try parseScore("87")
print(score)
} catch .emptyInput {
// ...
} catch .invalidNumber {
// ...
} catch .outOfRange {
// ...
}
// Error: switch must be exhaustive, .containsWhitespace tidak ditangani
Tanpa typed throws, menambah case error baru tidak menghasilkan diagnostic apa pun di call site. Caller cuma menangkap any Error dan menyembunyikan masalah. Ini salah satu kelas bug yang paling sering saya temui saat review code, dan saya kena bug ini langsung di project terakhir: error case baru ditambah di service layer, tapi UI tidak pernah memperbarui pesan untuk pengguna selama dua sprint. Typed throws menghilangkan kelas bug ini sepenuhnya selama tipe error tidak diubah menjadi any Error.
Typed throws vs Result type: mana yang dipakai?
Pertanyaan ini sering muncul karena Result<Success, Failure: Error> sudah lama menjadi pengganti tidak resmi untuk error handling typed. Keduanya sekarang punya overlap fungsional, tapi bukan duplikat. Tabel berikut membandingkan dimensi yang biasanya menentukan pilihan.
Dimensi
throws(MyError)
Result<T, MyError>
Sintaks pemanggilan
try fn() dengan do/catch
switch fn() atau .get()
Cocok untuk async
Ya, native
Ya, tapi membungkus dua kali
Bisa disimpan sebagai nilai
Tidak
Ya, first-class value
Cocok untuk callback API
Tidak ergonomis
Ya, idiomatis
Overhead runtime
Zero-cost dengan Never
Selalu mengalokasikan enum
Membutuhkan unwrap
Tidak, nilai return langsung
Ya, switch atau try .get()
Dukungan Swift Concurrency
Penuh dengan async throws
Manual lewat task
Aturan praktis yang saya pakai: gunakan throws(MyError) untuk fungsi yang dipanggil dalam alur kontrol normal, dan gunakan Result ketika error perlu disimpan, dikirim antar boundary, atau dijadikan first-class value. Misalnya untuk caching hasil sukses-atau-gagal, atau di completion handler API lama yang belum dimigrasi ke async/await. Untuk pembahasan lebih dalam tentang concurrency, lihat panduan approachable concurrency Swift 6.2.
Typed throws di fungsi async
Typed throws bekerja mulus dengan async. Gabungannya menjadi async throws(MyError). Inilah yang membuat fitur ini bermakna untuk kode iOS modern: actor method, Task body, dan AsyncSequence semuanya bisa dipersempit ke error domain spesifik.
enum NetworkError: Error {
case offline
case timeout
case decoding(any Error)
}
actor APIClient {
func fetchUser(id: Int) async throws(NetworkError) -> User {
guard await isOnline() else { throw NetworkError.offline }
do {
let data = try await session.data(from: endpoint(for: id)).0
return try JSONDecoder().decode(User.self, from: data)
} catch let decodingError as DecodingError {
throw NetworkError.decoding(decodingError)
} catch {
throw NetworkError.timeout
}
}
}
Task {
do {
let user = try await client.fetchUser(id: 42)
await render(user)
} catch .offline {
await showOfflineBanner()
} catch .timeout {
await showRetry()
} catch .decoding(let underlying) {
await report(underlying)
}
}
Perhatikan pola NetworkError.decoding(any Error). Ini cara membungkus error dari lapisan lain (misalnya DecodingError dari Foundation) tanpa membocorkan tipe internalnya ke caller. Pattern ini umum di library publik: error domain Anda tertutup, tapi tetap menyimpan jejak diagnostik dari error yang lebih rendah.
Migrasi dari throws lama ke typed throws
Salah satu desain paling penting dari typed throws adalah opsionalitas penuh. Kode lama yang menulis throws tanpa anotasi tetap valid dan secara semantik setara dengan throws(any Error). Artinya, Anda bisa mengadopsi typed throws bertahap, fungsi demi fungsi, tanpa harus mengubah seluruh codebase sekaligus. Berikut langkah migrasi yang saya pakai di project produksi.
Mulai dari lapisan terdalam. Fungsi utility kecil yang melempar 2-3 jenis error adalah kandidat ideal pertama. Perubahannya terbatas dan keuntungannya terlihat segera di caller terdekat.
Buat enum error per modul. Hindari satu enum AppError raksasa; bagi per fitur (AuthError, SyncError, ImportError). Enum kecil memungkinkan exhaustive catch yang benar-benar berarti.
Ganti signature secara bertahap. Ubah func foo() throws menjadi func foo() throws(FooError). Compiler akan menandai semua titik di mana fungsi tersebut melempar error di luar FooError, jadi Anda bisa membungkus atau mengkonversi di sana.
Refactor call site setelah signature stabil. Block do/catch di caller bisa langsung memakai pattern matching enum tanpa cast.
Biarkan boundary publik melempar any Error. Top-level handler (App delegate, view event handler) sering harus menangani banyak jenis error tidak terkait. Typed throws di sana cuma menambah kebisingan.
Pendekatan bertahap ini juga kompatibel dengan strategi testing Anda. Sintaks #expect(throws:) di framework Swift Testing baru menerima tipe error spesifik dan sangat cocok dengan typed throws. Lihat panduan migrasi Swift Testing dari XCTest untuk contoh assertion pada error typed.
Kapan pakai dan kapan menghindari typed throws
Typed throws bukan default baru. Tim core Swift sengaja menempatkannya sebagai opt-in, dan itu pilihan desain yang saya setujui. Berikut heuristik praktis berdasarkan pengalaman menggunakannya di beberapa codebase produksi sejak beta Xcode 26.
Pakai typed throws ketika:
Anda menulis library publik dengan error domain yang stabil dan kecil (3-7 case).
Caller perlu memberikan response berbeda untuk setiap jenis error (UI berbeda, retry policy berbeda).
Fungsi adalah leaf node di call graph, jadi tidak memanggil banyak fungsi lain yang melempar error berbeda-beda.
Anda ingin compiler memandu refactor saat error domain berkembang.
Hindari typed throws ketika:
Fungsi memanggil banyak dependensi dari modul lain dengan error type beragam. Anda akan menghabiskan waktu hanya untuk membungkus error.
Caller tidak peduli dengan tipe error spesifik dan cuma akan menampilkan pesan generik.
Fungsi adalah entry point UI yang menangani semua error secara seragam (log + tampilkan dialog).
Anda sedang prototyping cepat. Tetap pakai throws biasa dan migrasi nanti jika perlu.
Dokumentasi resmi Swift blog dan rilis catatan Xcode 26 menekankan poin yang sama: typed throws ditujukan untuk situasi spesifik, bukan menggantikan throws generik secara universal.
throws(Never) dan optimasi zero-cost
Salah satu detail elegan dari desain typed throws adalah throws(Never). Karena Never tidak punya instance, fungsi dengan signature func foo() throws(Never) -> T secara matematis setara dengan fungsi non-throwing, dan compiler memperlakukannya seperti itu. Tidak ada overhead runtime, tidak ada error metadata di stack frame, tidak ada path keluar selain return normal.
// Fungsi generik yang menerima throwing closure
func retry<E: Error>(
times: Int,
_ operation: () throws(E) -> Int
) throws(E) -> Int {
var lastError: E?
for _ in 0..<times {
do {
return try operation()
} catch {
lastError = error
}
}
throw lastError!
}
// Pemanggilan dengan closure non-throwing, E ter-infer ke Never
let result = retry(times: 3) { 42 }
// Compiler tahu retry tidak akan pernah throw, jadi tidak butuh try
Pattern throws(E) di tanda tangan generik adalah cara modern menulis higher-order function yang transparan terhadap error. Closure non-throwing tetap non-throwing, closure throwing tetap throwing, semua dalam satu definisi. Standard library Swift sudah mulai mengadopsi pola ini di fungsi seperti map, filter, dan reduce di Swift 6.2.
Frequently Asked Questions
Apakah typed throws menggantikan Result type di Swift?
Tidak. Typed throws dan Result punya use case berbeda. Typed throws cocok untuk alur kontrol normal dengan do/catch, sedangkan Result tetap relevan ketika error perlu disimpan sebagai nilai, dikirim antar thread, atau dipakai di completion handler API lama. Keduanya bisa hidup berdampingan dalam codebase yang sama.
Apakah typed throws breaking change untuk kode Swift lama?
Tidak. throws tanpa anotasi tetap berarti throws(any Error) seperti sebelumnya, jadi semua kode existing tetap kompil dan berperilaku sama. Adopsi typed throws sepenuhnya opt-in dan bisa dilakukan bertahap per fungsi.
Bagaimana cara test fungsi dengan typed throws di Swift Testing?
Pakai #expect(throws: MyError.self) atau pattern matching dengan #expect(throws: MyError.invalidInput). Karena typed throws membatasi tipe error di signature, test assertion menjadi lebih spesifik dan failure message lebih informatif dibanding any Error.
Apakah typed throws menambah ukuran binary aplikasi?
Tidak signifikan. Tipe error konkret menggantikan existential any Error di metadata fungsi, yang justru mengurangi overhead dispatch dinamis. Fungsi throws(Never) dioptimasi compiler menjadi setara fungsi non-throwing tanpa overhead runtime sama sekali.
Kapan typed throws tersedia di production?
Typed throws stabil di Swift 6.2, yang dirilis bersama Xcode 26 pada September 2025 dan menjadi compiler default untuk project iOS 26. Anda bisa memakainya hari ini di project mana pun yang menargetkan iOS 18.0 ke atas; toolchain Swift 6.2 mendukung deployment ke OS lama.
AlarmKit di iOS 26 memungkinkan aplikasi pihak ketiga bikin alarm dan timer yang menembus mode Focus dan silent. Panduan ini menunjukkan cara memakai AlarmManager, App Intents, dan AlarmPresentation dengan contoh kode Swift lengkap.
Panduan praktis NavigationStack di SwiftUI iOS 26: type-safe routing, NavigationPath, deep link parser, integrasi NavigationSplitView untuk iPad, plus catatan VoiceOver dari pengalaman production.
Panduan migrasi dari XCTest ke Swift Testing di iOS 26: macro @Test, #expect, #require, parameterized test, trait, plus pengalaman lapangan migrasi 480 test.