Skip to content

Swift Error Handling — Do, Catch, Try, and Throw Explained

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Swift Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift error handling provides a structured way to define, throw, and catch errors using do-catch blocks, throwing functions, and the try keyword, ensuring errors are handled explicitly rather than ignored or leading to crashes. This tutorial covers defining custom error types with the Error protocol, throwing errors, catching specific error cases, propagating errors with throws, converting errors to optionals with try?, and cleanup actions with defer.

What You'll Learn

  • Defining custom error types with the Error protocol and enums
  • Throwing errors with the throw keyword
  • Catching errors with do-catch blocks and pattern matching
  • Propagating errors with throwing functions
  • Using try?, try!, and try for different error handling styles
  • Cleaning up resources with defer statements
  • Rethrowing closures and functions
  • Throwing errors from initializers

Why It Matters

Error handling separates robust applications from fragile ones. Swift's do-catch system forces you to handle errors at compile time, unlike exceptions in many languages that can be silently ignored. Combined with Swift's pattern matching, you can handle specific error cases differently, providing appropriate feedback to users and graceful degradation when operations fail.

Real-World Use

Doda Browser makes extensive use of Swift error handling in its networking layer. Network errors, JSON decoding failures, and authentication failures are all represented as enum cases in a NetworkError type. The browser's error recovery system catches specific errors to retry requests, show cached content, or display user-friendly error messages.

Learning Path

flowchart LR
  A[Optionals] --> B[Error Handling\nYou are here]
  B --> C[Classes and Structs]
  style B fill:#f90,color:#fff

Defining Error Types

Errors conform to the Error protocol, typically using an enum:

import Foundation

enum FileError: Error {
  case notFound(path: String)
  case permissionDenied
  case invalidData(reason: String)
  case outOfSpace(required: Int, available: Int)
}

enum NetworkError: Error {
  case connectionTimeout
  case serverError(statusCode: Int)
  case notFound
  case unauthorized
  case decodingFailed(description: String)
}

Throwing and Catching

Use throw to trigger an error and do-catch to handle it:

import Foundation

enum CalculatorError: Error {
  case divisionByZero
  case negativeSquareRoot
  case overflow
}

func divide(_ a: Double, _ b: Double) throws -> Double {
  guard b != 0 else {
    throw CalculatorError.divisionByZero
  }
  return a / b
}

func squareRoot(_ value: Double) throws -> Double {
  guard value >= 0 else {
    throw CalculatorError.negativeSquareRoot
  }
  return sqrt(value)
}

do {
  let result1 = try divide(10, 2)
  print("Division result: \(result1)")

  let result2 = try divide(5, 0)
  print("This will not execute")
} catch CalculatorError.divisionByZero {
  print("Error: Cannot divide by zero")
} catch {
  print("Unexpected error: \(error)")
}

// Catching specific cases with pattern matching
do {
  let result = try squareRoot(-4)
} catch CalculatorError.negativeSquareRoot {
  print("Error: Cannot calculate square root of a negative number")
}

Output:

Division result: 5.0
Error: Cannot divide by zero

Multiple catch Patterns

Swift's pattern matching allows handling specific cases with associated values:

import Foundation

struct UserData {
  static func loadFromFile(at path: String) throws -> Data {
    let fileManager = FileManager.default

    guard fileManager.fileExists(atPath: path) else {
      throw FileError.notFound(path: path)
    }

    guard fileManager.isWritableFile(atPath: path) else {
      throw FileError.permissionDenied
    }

    guard let data = fileManager.contents(atPath: path) else {
      throw FileError.invalidData(reason: "Cannot read file contents")
    }

    return data
  }
}

do {
  let data = try UserData.loadFromFile(at: "/tmp/config.json")
  print("Loaded \(data.count) bytes")
} catch FileError.notFound(let path) {
  print("File not found at: \(path)")
} catch FileError.permissionDenied {
  print("Permission denied - check file access rights")
} catch FileError.invalidData(let reason) {
  print("Invalid data: \(reason)")
} catch {
  print("Unknown error: \(error)")
}

Output:

File not found at: /tmp/config.json

Propagating Errors

Functions that can throw must be marked with throws:

import Foundation

enum ValidationError: Error {
  case invalidEmail
  case passwordTooShort(minLength: Int)
  case usernameTaken
}

func validateEmail(_ email: String) throws {
  guard email.contains("@") && email.contains(".") else {
    throw ValidationError.invalidEmail
  }
}

func validatePassword(_ password: String) throws {
  guard password.count >= 8 else {
    throw ValidationError.passwordTooShort(minLength: 8)
  }
}

func registerUser(email: String, password: String, username: String) throws -> String {
  try validateEmail(email)
  try validatePassword(password)

  // Simulate username check
  let takenUsernames = ["admin", "root"]
  if takenUsernames.contains(username.lowercased()) {
    throw ValidationError.usernameTaken
  }

  return "User \(username) registered successfully"
}

do {
  let result = try registerUser(
    email: "alice@example.com",
    password: "secure123",
    username: "alice"
  )
  print(result)
} catch ValidationError.invalidEmail {
  print("Please provide a valid email address")
} catch ValidationError.passwordTooShort(let minLen) {
  print("Password must be at least \(minLen) characters")
} catch ValidationError.usernameTaken {
  print("This username is already taken")
}

Output:

User alice registered successfully

Try? and Try!

Convert throwing expressions to optionals or disable error propagation:

import Foundation

// try? converts error to nil
func parseInt(_ str: String) -> Int? {
  return Int(str)
}

let number1 = parseInt("42")
let number2 = parseInt("hello")
print("Parsed: \(number1 ?? 0), Invalid: \(number2 ?? 0)")

// try? with throwing functions
enum MathError: Error {
  case outOfBounds
}

func safeSqrt(_ value: Double) throws -> Double {
  guard value >= 0 else { throw MathError.outOfBounds }
  return sqrt(value)
}

let result = try? safeSqrt(25)
print("Sqrt result: \(result ?? 0)")

let invalid = try? safeSqrt(-1)
print("Invalid sqrt: \(invalid ?? 0)")

// try! - only use when you are certain it won't fail
let definitelyValid = try! safeSqrt(16)
print("Definite: \(definitelyValid)")

Output:

Parsed: 42, Invalid: 0
Sqrt result: 5.0
Invalid sqrt: 0
Definite: 4.0

Defer for Cleanup

Defer executes regardless of whether an error is thrown:

import Foundation

func processFile(path: String) throws {
  print("Opening file: \(path)")
  defer {
    print("Closing file: \(path)")
  }

  guard path.hasSuffix(".txt") else {
    throw FileError.invalidData(reason: "Wrong file extension")
  }

  // Simulate processing
  print("Processing file contents...")
  // defer runs here on success, or wherever we exit
}

do {
  try processFile(path: "/tmp/data.txt")
} catch {
  print("Error: \(error)")
}

print("---")

do {
  try processFile(path: "/tmp/data.json")
} catch {
  print("Error: \(error)")
}

Output:

Opening file: /tmp/data.txt
Processing file contents...
Closing file: /tmp/data.txt
---
Opening file: /tmp/data.json
Closing file: /tmp/data.json
Error: invalidData(reason: "Wrong file extension")

Common Mistakes

  1. Not handling specific error cases: Catching all errors with a single catch { } loses information about what went wrong. Catch specific cases where possible.
  2. Using try! in production code: try! crashes on errors. Only use it when you are absolutely certain the operation cannot fail (e.g., hardcoded data).
  3. Forgetting that throws functions cannot be called without try: The compiler forces you to handle errors from throwing functions.
  4. Not cleaning up resources before throwing: If you allocate a resource and then throw, the resource leaks. Use defer for cleanup.
  5. Error enums without associated values: Without associated values, the caller cannot determine contextual information like which file was not found.

Practice Questions

  1. What protocol must error types conform to?

    • The Error protocol. Enums are the most common way to define error types.
  2. What is the difference between try, try?, and try!?

    • try requires do-catch handling. try? converts the result to an optional (nil on error). try! crashes on error.
  3. What does the defer keyword do?

    • defer schedules a block of code to execute when the current scope exits, regardless of how it exits (return, throw, break).
  4. Can initializers throw errors?

    • Yes. Mark initializers with init? (failable) or init() throws (throwing) to handle construction failures.
  5. Challenge: Write a JSON configuration loader that throws specific errors for missing file, malformed JSON, and missing required keys. Use defer to close the file handle.

Mini Project

Build a robust file manager with error handling:

  • Define a FileManagerError enum with cases for notFound, permissionDenied, diskFull, and invalidPath.
  • Create a SafeFileManager class with methods: readFile, writeFile, copyFile, deleteFile.
  • Each method throws appropriate errors with associated values.
  • Use defer for file handle cleanup.
  • Add a method that attempts multiple operations and collects all errors into an array.
  • Write unit tests that verify each error case is thrown correctly.

FAQ

Can I throw from a closure?

Yes, but the closure must be marked as throws and cannot be passed to a non-throwing parameter unless it uses rethrows.

What is the difference between errors and optionals?

Errors represent unexpected failures that should be handled. Optionals represent the absence of a value by design. Use optionals for simple missing values, errors for operational failures.

How do I define error types in a library?

Make your error enums public and document each case. Include associated values with contextual information like which parameter was invalid.

What's Next

Learn about value types vs reference types in the Classes and Structs tutorial.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro