Labs ICT
โญ Pro Login

Error Handling

Catching and throwing errors gracefully.

Error Handling in Swift

Every app runs into problems โ€” a network request fails, a file doesn't exist, or user input is invalid. Swift gives you a clean way to handle these situations instead of crashing. The Error protocol is the foundation. Any type that conforms to Error can be thrown, caught, and recovered from gracefully.

enum NetworkError: Error {
    case badURL
    case noConnection
    case serverDown(statusCode: Int)
}

func fetchData(from urlString: String) throws -> Data {
    guard let url = URL(string: urlString) else {
        throw NetworkError.badURL
    }
    // ... fetch data
    return Data()
}
Try it Yourself ->

throw and throws

The throws keyword on a function signature tells Swift this function might fail. Inside the function, use throw to actually send an error back to the caller. Think of it as a signal that says "something went wrong, deal with it."

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else {
        throw NSError(domain: "Math", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot divide by zero"])
    }
    return a / b
}
Try it Yourself ->

do, try, catch

When you call a throwing function, you wrap it in a do block and use try before the call. If something goes wrong, the catch block handles it. This is Swift's way of saying "try this, but be ready to recover."

do {
    let result = try divide(10, by: 0)
    print("Result: \(result)")
} catch NetworkError.badURL {
    print("Invalid URL")
} catch {
    print("Something else went wrong: \(error)")
}
Try it Yourself ->

try? and try!

Sometimes you don't need the error details. try? converts the result to an optional โ€” returns nil on failure. try! crashes if it fails, so only use it when you're absolutely certain it won't.

// try? โ€” safe, returns optional
let data = try? fetchData(from: "https://example.com")

// try! โ€” crashes on failure, use only when guaranteed
let forcedData = try! fetchData(from: "https://example.com")
Try it Yourself ->

Custom Error Types

Enums are perfect for modeling errors. Each case can carry associated values to give context about what went wrong. This makes debugging much easier because you know exactly what happened.

enum ValidationError: Error, LocalizedError {
    case emptyField(fieldName: String)
    case invalidEmail
    case passwordTooShort(minLength: Int)

    var errorDescription: String? {
        switch self {
        case .emptyField(let name):
            return "\(name) cannot be empty."
        case .invalidEmail:
            return "Please enter a valid email address."
        case .passwordTooShort(let min):
            return "Password must be at least \(min) characters."
        }
    }
}

func validate(email: String) throws {
    guard !email.isEmpty else {
        throw ValidationError.emptyField(fieldName: "Email")
    }
    guard email.contains("@") else {
        throw ValidationError.invalidEmail
    }
}
Try it Yourself ->

Rethrowing Functions

A rethrowing function doesn't throw on its own but passes along errors from a closure you give it. This is how Swift stays flexible โ€” you can wrap throwing code without adding your own error paths.

func perform(_ operation: () throws -> T) rethrows -> T {
    return try operation()
}

do {
    let value = try perform { try divide(10, by: 2) }
    print(value)
} catch {
    print(error)
}
Try it Yourself ->

๐Ÿงช Quick Quiz

What keyword marks a function that can throw errors?