Understanding Error Handling in Swift: Why do-try-catch is the Best Approach

Light grey background image with the Swift bird logo, representing error handling in programming.
Adarsh
iOS Engineer
|
February 13, 2025
iOS
Swift

Error handling is a crucial part of programming, and Swift provides multiple ways to deal with potential failures. Many beginners struggle to understand why do-try-catch exists when we already have other mechanisms like Optionals, Tuples, and Result enums.

In this blog, we’ll go step by step, analyzing different error-handling approaches and showing why do-try-catch is the cleanest and most structured way to handle errors in Swift.

Step 1: Understanding the Problem — isActive Flag

We will use a class called DoTryCatchDataManager, which has a flag isActive.

  • If isActive = true, it returns valid data.
  • If isActive = false, it simulates a failure, returning either nil, an error, or throwing an error.

Function 1: Using Optionals (getTitle1())

func getTitle1() -> String? {
if isActive {
    return "Title 1"
    } else {
      return nil  // No title if isActive is false
    }
}


Problem with Optionals

This function returns an Optional String (String?), which means:

  • If isActive = true, it returns "Title 1".
  • If isActive = false, it returns nil.

How Do We Handle This?

To use this function safely, we need optional unwrapping (if let, guard let, or ?? operator).
Example:

let title = getTitle1() ?? "Error: No Title"


What’s Wrong with This Approach?

It doesn’t tell us why it failed. We just get nil, but we don't know what went wrong.
Forces developers to unwrap the optional. Beginners often forget to handle nil, which leads to unexpected crashes.

Function 2: Using Tuples (getTitleTitle2())

func getTitleTitle2() -> (title: String?, error: Error?) {
    if isActive {
        return ("Title 2", nil)
    } else {
        return (nil, URLError(.badURL))  // Simulating error
    }
}


What Changed?

Now, the function returns a tuple:

  • title: → The actual value (if successful).
  • error: Error? → The error (if something goes wrong).

How Do We Handle This?

We need to manually check both values:

let result = getTitleTitle2()
if let title = result.title {
    print("Success: \(title)")
} else {
    print("Error: \(result.error?.localizedDescription ?? "Unknown Error")")
}

What’s Wrong with This Approach?

Still requires manual error checking. We need to handle both the title and error manually.
Not Swift-like. It feels like we are simulating an error-handling system instead of using Swift’s built-in error-handling mechanism.

Function 3: Using Result Enum (getTitle3())

func getTitle3() -> Result<String, Error> {
    if isActive {
        return .success("Title 3")
    } else {
        return .failure(URLError(.badURL))  // Simulating failure
    }
}


What Changed?

This function now returns Swift’s built-in Result type, which can either be:

  • .success(value) → Contains the title.
  • .failure(error) → Contains the error.

How Do We Handle This?

We use a switch case:

switch getTitle3() {
case .success(let title):
    print("Success: \(title)")
case .failure(let error):
    print("Error: \(error.localizedDescription)")
}


What’s Wrong with This Approach?

Better than Optionals & Tuples because it forces us to handle both success and failure.

Still requires a switch case. The error-handling logic is separated from where the function is called.

Becomes complex when dealing with multiple function calls. Imagine handling five functions this way — our code would become messy with switch cases everywhere.

Function 4: Using do-try-catch (getTitle4())

func getTitle4() throws -> String {
    if isActive {
        return "Title 4"
    } else {
        throw URLError(.badURL)
    }
}


What Changed?

  • Instead of returning nil, a tuple, or a Result, this function throws an error if something goes wrong.
  • This means Swift enforces error handling.

How Do We Handle This?

We must use do-try-catch:

do {
    let title = try getTitle4()
    print("Success: \(title)")
} catch {
    print("Error: \(error.localizedDescription)")
}


Why do-try-catch is the Best Approach?

Forces error handling. Unlike Optionals, we cannot ignore the error.

No unnecessary switch case. We just try the function and handle the error in catch.

Easier to read & maintain. Swift’s built-in error handling mechanism keeps the code structured and clear.

Comparing All Four Approaches

Final Thoughts: When to Use What?

Conclusion: Why do-try-catch is Beautiful

  • It forces error handling, so no errors are ignored.
  • It keeps code clean, without messy if let, switch, or tuple checks.
  • It uses Swift’s built-in error system, making it easy to integrate with other libraries.

By understanding why do-try-catch exists, beginners can appreciate how Swift encourages safer and more structured error handling.

Feel free to drop your thoughts and comments on my Medium blog here and for any project related inquiries, contact us below.

Recommended Posts

Black and white image of a desk with a computer monitor displaying code, surrounded by floating tech diagrams and a large light bulb, conveying innovation.
Swift
iOS
Exploring MVVM and MVC Architectures in Swift
Feb 08, 2024
|
The Gemini logo is in the center on a black background. Below it, swirling white lines converge and diverge symmetrically, creating a dynamic, futuristic design.
AI
LLM
Gemini: A guide to Google’s free AI LLM offering in 2024
Feb 26, 2024
|
Sanskar

Ready to get started?

Schedule a discovery call and see how we’ve helped hundreds of SaaS companies grow!