Learn Swift - Error Handling & Debugging
Episode 6 of 23

Learn Swift - Error Handling & Debugging

This episode covers error handling in Swift with throws, try, catch, and defer, creating custom error types, and common error patterns. You will also learn debugging in Xcode with breakpoints and LLDB, plus assertions and unit tests to validate program behavior.

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

Introduction

A good program doesn't just run when everything is right — it also behaves gracefully when things go wrong. Episode 6 covers error handling and debugging: how Swift models failures explicitly with throws, try, and catch, and how you track down and fix bugs with breakpoints, LLDB, and tests.

One principle you'll hold onto forever: errors in Swift are never hidden. A function that can fail declares its capability through its type, and the compiler forces callers to handle it. This contrasts with languages that let failures stay invisible until a crash in production.

Basic Error Handling

throws, try, and catch

Functions that can throw errors are marked throws, and callers handle them with do-catch:

do-catch with throws
enum KesalahanBagi: Error {
    case pembagiNol
}
 
func bagi(_ a: Int, _ b: Int) throws -> Int {
    guard b != 0 else {
        throw KesalahanBagi.pembagiNol
    }
    return a / b
}
 
do {
    let hasil = try bagi(10, 0)
    print(hasil)
} catch KesalahanBagi.pembagiNol {
    print("Pembagi tidak boleh nol")
}

try bagi(10, 0) calls a potentially error-throwing function inside a do block. When an error is thrown, execution moves to the matching catch block. Proper handling turns failure into a controlled flow.

defer: Guaranteed Cleanup

When an error can be thrown in the middle of a function, resource cleanup must still happen. The defer block runs its code when leaving the scope, no matter which path is taken:

defer for cleanup
func bacaFile(path: String) throws -> String {
    let file = open(path)
    defer {
        close(file)
        print("File ditutup")
    }
    // Operasi yang bisa melempar
    return "Isi file"
}

defer { close(file) } ensures close(file) is called whether the function returns a value or throws an error. Multiple defer blocks execute in the reverse order of their declaration — keep this in mind when arranging nested initializations.

Custom Errors and Common Patterns

Creating Your Own Error Type

An error in Swift is any type that adopts the Error protocol. Best practice: use enums with associated values to carry context:

Error with context
enum ErrorJaringan: Error {
    case tidakAdaKoneksi
    case statusBuruk(code: Int)
    case payloadTidakValid(pesan: String)
}
 
func kirimRequest() throws {
    throw ErrorJaringan.statusBuruk(code: 503)
}
 
do {
    try kirimRequest()
} catch ErrorJaringan.statusBuruk(let code) where code >= 500 {
    print("Server bermasalah: \(code)")
} catch {
    print("Error lain: \(error)")
}

catch ErrorJaringan.statusBuruk(let code) unpacks the associated value and combines it with where for filtering. Catching all other errors with a bare catch ensures no failure goes unnoticed.

Common Pattern: Result

For asynchronous operations, the classic throws pattern doesn't apply because errors can't be thrown across closure boundaries. Instead, Swift provides the Result enum:

Result as operation outcome
func proses(data: String) -> Result<Int, Error> {
    guard let angka = Int(data) else {
        return .failure(ErrorJaringan.payloadTidakValid(pesan: data))
    }
    return .success(angka * 2)
}
 
let hasil = proses(data: "21")
print(hasil)

Result<Int, Error> wraps success or failure in a single value. Episodes 8 and 9 will use this pattern alongside async/await for networking and concurrency.

Debugging in Xcode

Breakpoints and LLDB

Xcode ships with the integrated LLDB debugger. You can halt execution with breakpoints, then inspect state from the console. A set of commands worth memorizing:

Basic LLDB commands
breakpoint set --file main.swift --line 10
run
print skor
step
next
continue

breakpoint set --file main.swift --line 10 sets a breakpoint before running the program. In the LLDB console, print skor displays the current variable value, next executes the following line, and continue resumes execution until the next breakpoint.

Reading a Stack Trace

When a crash occurs, the stack trace shows the chain of function calls. Start reading from the topmost frame (the crash location), then work downward looking for frames belonging to your code — frames from Apple frameworks are usually safe to skip. Xcode's automatic symbolication translates memory addresses back into function names and lines.

Assertions and Unit Tests

Assertions for Internal Contracts

assert and precondition enforce invariants that, when violated, mean there's a bug:

assert and precondition
func prosesNilai(_ nilai: Int) {
    precondition(nilai >= 0, "nilai harus non-negatif")
    assert(nilai < 1000, "nilai terlalu besar untuk diproses")
    print("Memproses \(nilai)")
}
 
prosesNilai(42)

precondition(nilai >= 0) is always active, even in production builds — suitable for conditions that must never be violated. assert is only active in debug builds and removed on release, useful for expensive checks. Use both to find bugs earlier.

Quick Verification with Unit Tests

Although episode 15 covers testing in depth, you can start verifying behavior now with XCTest:

Simple test
import XCTest
 
final class BagiTests: XCTestCase {
    func testPembagianNormal() throws {
        let hasil = try bagi(10, 2)
        XCTAssertEqual(hasil, 5)
    }
}

XCTAssertEqual(hasil, 5) checks a function's result. Tests become a safety net when you change code — every time a feature changes, tests tell you whether old behavior broke.

Tip

Start building the habit of writing tests alongside features, not afterwards. Tests written after a bug is found often just lock in the wrong behavior.

Closing

Key takeaways:

  • Potentially error-throwing functions are marked throws, called with try, and handled in do-catch.
  • defer blocks guarantee resource cleanup no matter which execution path is taken.
  • The best error types are built with enums and associated values.
  • Result wraps success and failure for asynchronous operations.
  • LLDB and Xcode breakpoints enable interactive state inspection.
  • precondition and assert enforce internal invariants early.

In the next episode, episode 7, we'll cover data persistence and file I/O — working with FileManager and URL to read and write local files, serialization with the Codable protocol for JSON and plist, lightweight storage with UserDefaults and Keychain, and an overview of Core Data for complex persistence. Your data will start being stored safely!

Learn Swift - Error Handling & Debugging | Learn Swift