# Swift do-catch Blocks: Error Handling Syntax and Examples

Source: https://devdocs10.com/articles/swift/swift-do-catch-blocks  
By Murat Bo · Published 2026-06-01 · DevDocs10 (https://devdocs10.com)

Swift `do-catch` blocks are the core language feature for handling thrown errors in a safe, readable way. They let you run code that might fail, react to specific errors, and keep your program under control instead of crashing or ignoring problems. In this article, you will learn what `do-catch` blocks are, how they work with `throw` and `try`, when to use them, and the mistakes beginners commonly make.

> **Quick answer:** In Swift, use a `do-catch` block when you call code marked `throws` and you want to handle any error it produces. Put the throwing code inside `do`, prefix the throwing call with `try`, and handle matching errors in one or more `catch` blocks.

> **Difficulty:** Beginner
>
> **Helpful to know first:** You will understand this better if you already know basic Swift functions, enums, conditionals, and how values are passed into functions.

## 1. What Is do-catch in Swift?

A `do-catch` block is Swift’s structured way to handle errors from code that can fail. It works together with functions marked `throws` and calls marked `try`.

- `do` contains code that may throw an error.
- `try` marks a call that can fail by throwing.
- `catch` handles the error if one is thrown.
- Errors in Swift are values, usually defined with an `enum` that conforms to `Error`.
- `do-catch` is different from optional error handling with `try?` and forced error handling with `try!`.

Think of it as a clear contract: a function says, “I might fail,” and the caller decides how to respond. That is much safer than silently ignoring problems.

```swift
enum LoginError: Error {
    case invalidPassword
    case accountLocked
}

func signIn(password: String) throws {
    if password != "secret123" {
        throw LoginError.invalidPassword
    }
}

do {
    try signIn(password: "guess")
    print("Signed in successfully")
} catch {
    print("Sign-in failed:", error)
}
```

Output (tested with Swift 6.4):

```text
Sign-in failed: invalidPassword
```

This example shows the full basic pattern: a throwing function, a call with `try`, and a `catch` block that receives the thrown error.

## 2. Why do-catch Matters

Error handling matters because many real operations can fail for valid reasons: reading a file, decoding data, validating input, parsing a number, or checking business rules. `do-catch` lets you handle these failures intentionally.

Use `do-catch` when:

- You need to show a friendly message when an operation fails.
- You want different behavior for different error types.
- You need to recover and continue running.
- You want the code path for success and failure to stay explicit and readable.

Do not use `do-catch` for ordinary true-or-false conditions that are not really exceptional. For example, checking whether a list is empty usually does not need throwing errors.

> A useful rule of thumb is this: use throwing errors when failure is a real possibility that the caller should handle, not just a normal branch of everyday logic.

## 3. Basic Syntax

### Mark a function as throwing

A function that can fail must be marked with `throws`.

```swift
enum AgeError: Error {
    case invalidAge
}

func register(age: Int) throws {
    if age < 13 {
        throw AgeError.invalidAge
    }
}
```

This tells Swift that calling `register` might produce an error instead of completing normally.

### Call it with try inside do

When you call a throwing function, prefix the call with `try` and place it in a `do` block if you want to catch the error locally.

```swift
do {
    try register(age: 10)
    print("Registration complete")
} catch {
    print("Registration failed:", error)
}
```

Output (tested with Swift 6.4):

```text
Registration failed: invalidAge
```

If `register` throws, Swift jumps directly to the `catch` block.

### Catch specific errors

You can match particular error cases for more precise handling.

```swift
do {
    try register(age: 10)
} catch AgeError.invalidAge {
    print("You must be at least 13 years old.")
} catch {
    print("Unexpected error:", error)
}
```

Output (tested with Swift 6.4):

```text
You must be at least 13 years old.
```

This version is more helpful because it responds differently to a known problem.

## 4. Step-by-Step Examples

### Example 1: Validating user input

This example uses a custom error enum to reject blank usernames.

```swift
enum UsernameError: Error {
    case empty
}

func validateUsername(_ name: String) throws {
    if name.isEmpty {
        throw UsernameError.empty
    }
}

do {
    try validateUsername("")
    print("Username is valid")
} catch UsernameError.empty {
    print("Username cannot be empty")
} catch {
    print("Unknown validation error")
}
```

Output (tested with Swift 6.4):

```text
Username cannot be empty
```

The thrown error clearly represents why validation failed, and the matching `catch` makes the response readable.

### Example 2: Parsing a number from text

Sometimes input looks valid but cannot be converted into the type you need.

```swift
enum ParseError: Error {
    case notANumber
}

func parseScore(from text: String) throws -> Int {
    guard let score = Int(text) else {
        throw ParseError.notANumber
    }

    return score
}

do {
    let value = try parseScore(from: "42")
    print("Parsed score:", value)
} catch {
    print("Could not parse score:", error)
}
```

Output (tested with Swift 6.4):

```text
Parsed score: 42
```

Here, the success path returns an `Int`, while the failure path throws a meaningful error.

### Example 3: Handling multiple specific errors

You can use more than one `catch` block to handle different cases differently.

```swift
enum PaymentError: Error {
    case insufficientFunds
    case cardExpired
}

func processPayment(balance: Double, amount: Double, cardIsValid: Bool) throws {
    if !cardIsValid {
        throw PaymentError.cardExpired
    }

    if balance < amount {
        throw PaymentError.insufficientFunds
    }
}

do {
    try processPayment(balance: 20.0, amount: 50.0, cardIsValid: true)
    print("Payment completed")
} catch PaymentError.insufficientFunds {
    print("Not enough balance for this payment.")
} catch PaymentError.cardExpired {
    print("Your card has expired.")
} catch {
    print("Payment failed for another reason.")
}
```

Output (tested with Swift 6.4):

```text
Not enough balance for this payment.
```

This pattern is common in apps where different failures need different messages or recovery steps.

### Example 4: Returning a value from a do block

You will often use `do-catch` when the throwing function also returns data.

```swift
enum DiscountError: Error {
    case invalidCode
}

func discountPercent(for code: String) throws -> Int {
    if code == "SAVE10" {
        return 10
    }

    throw DiscountError.invalidCode
}

do {
    let percent = try discountPercent(for: "SAVE10")
    print("Discount:", percent, "%")
} catch {
    print("Discount could not be applied:", error)
}
```

Output (tested with Swift 6.4):

```text
Discount: 10 %
```

The result is available only if the throwing call succeeds. If it fails, control moves directly to `catch`.

## 5. Practical Use Cases

- Validating form input before creating an account or submitting data.
- Parsing user-entered text into numbers, dates, or other structured values.
- Applying business rules such as purchase limits, permissions, or discount eligibility.
- Reading files, decoding JSON, or loading saved app data when failure is possible.
- Building library code where callers need clear, typed reasons for failure.
- Handling multiple failure cases with different user-facing messages.

## 6. Common Mistakes

### Mistake 1: Calling a throwing function without try

Every call to a function marked `throws` must be explicitly marked with `try`, unless you pass the error onward from another throwing context in the correct way.

**Problem:** Swift does not let throwing calls happen silently. You will get an error like `Call can throw, but it is not marked with 'try' and the error is not handled`.

```swift
enum FileError: Error {
    case missing
}

func loadFile() throws {
    throw FileError.missing
}

loadFile()
```

**Fix:** Add `try` and handle the error with `do-catch`.

```swift
do {
    try loadFile()
} catch {
    print("Could not load file:", error)
}
```

The corrected version works because the throwing call is now clearly marked and the error is handled.

### Mistake 2: Using try without a throwing function

Beginners sometimes add `try` to ordinary function calls because they think it is needed for all work inside a `do` block.

**Problem:** `try` is only for calls that can actually throw. Using it on non-throwing code causes a compiler error such as `No calls to throwing functions occur within 'try' expression`.

```swift
func greet() {
    print("Hello")
}

do {
    try greet()
} catch {
    print(error)
}
```

Output (tested with Swift 6.4):

```text
Hello
```

**Fix:** Remove `try` and the unnecessary `do-catch` if the code cannot throw.

```swift
func greet() {
    print("Hello")
}

greet()
```

Output (tested with Swift 6.4):

```text
Hello
```

The corrected version works because plain, non-throwing functions do not need Swift’s error-handling syntax.

### Mistake 3: Catching too generally when specific handling is needed

A single generic `catch` is valid, but sometimes it hides useful information about why an operation failed.

**Problem:** If you always use a generic catch, your app may show vague messages and lose the ability to recover differently for different error cases.

```swift
enum NetworkError: Error {
    case offline
    case timeout
}

func fetchProfile() throws {
    throw NetworkError.offline
}

do {
    try fetchProfile()
} catch {
    print("Something went wrong")
}
```

Output (tested with Swift 6.4):

```text
Something went wrong
```

**Fix:** Match the specific errors you expect first, then keep a general catch for anything else.

```swift
do {
    try fetchProfile()
} catch NetworkError.offline {
    print("You appear to be offline.")
} catch NetworkError.timeout {
    print("The request timed out.")
} catch {
    print("Unexpected network error:", error)
}
```

The corrected version works because each known error gets the most useful response.

### Mistake 4: Using try! when failure is possible

`try!` tells Swift to crash if an error is thrown. That is only appropriate when you are completely certain the call cannot fail in that situation.

**Problem:** If the function throws, your program stops with a runtime crash instead of handling the problem safely.

```swift
let score = try! parseScore(from: "not a number")
print(score)
```

**Fix:** Use `do-catch` when failure is a real possibility, or `try?` when turning failure into `nil` is appropriate.

```swift
do {
    let score = try parseScore(from: "not a number")
    print(score)
} catch {
    print("Invalid score input")
}
```

The corrected version works because the error is handled instead of causing a forced crash.

## 7. Best Practices

### Use specific error types

Clear error types make `catch` blocks more useful and your APIs easier to understand. Prefer descriptive enum cases over vague, generic failures.

```swift
enum PasswordError: Error {
    case tooShort
    case missingNumber
}

func validatePassword(_ password: String) throws {
    if password.count < 8 {
        throw PasswordError.tooShort
    }
}
```

This makes the caller’s error handling more informative and maintainable.

### Catch the errors you can actually handle

If you know how to recover from some errors but not others, match those cases specifically and let a general `catch` cover the rest.

```swift
do {
    try validatePassword("abc")
} catch PasswordError.tooShort {
    print("Password must be at least 8 characters.")
} catch {
    print("Password validation failed:", error)
}
```

Output (tested with Swift 6.4):

```text
Password must be at least 8 characters.
```

This keeps your error handling precise without becoming brittle.

### Use try? only when losing error details is acceptable

`try?` converts a thrown error into an optional `nil`. That is convenient, but it removes the reason for failure.

```swift
let score = try? parseScore(from: "25")
print(score ?? 0)
```

This is a good fit when you only care whether you got a value, not why parsing failed.

## 8. Limitations and Edge Cases

- `do-catch` only handles thrown errors. It does not catch logic bugs, forced unwrap crashes, or out-of-bounds crashes.
- A `catch` block only runs if something inside the related `do` actually throws.
- If you use `try?`, the error reason is discarded and replaced with `nil`.
- If you use `try!` and the call throws, your program crashes at runtime.
- You can rethrow errors from one function to another instead of handling them immediately, but then the outer caller must deal with them.
- Some APIs use optionals or result values instead of throwing, so not every failure in Swift is handled with `do-catch`.

> A common beginner confusion is expecting `do-catch` to behave like exception handling in some other languages. Swift error handling is explicit and only applies to operations marked as throwing.

## 9. do-catch vs try? vs try!

These three forms all relate to throwing functions, but they behave very differently.

| Form | What it does | Use when |
| --- | --- | --- |
| `do { try ... } catch` | Handles errors explicitly | You need the reason for failure or custom recovery |
| `try?` | Converts errors into `nil` | You only care whether a value was returned |
| `try!` | Crashes if an error is thrown | You are absolutely certain the call cannot fail |

In most real application code, `do-catch` is the safest and clearest choice because it keeps failure visible and lets you respond intelligently. Use `try?` for simple optional-style fallbacks. Use `try!` rarely and carefully.

## 10. Practical Mini Project

This mini project builds a simple coupon validator. It checks for an empty code, a code that is too short, and an unknown code, then prints a specific message for each failure.

```swift
enum CouponError: Error {
    case emptyCode
    case codeTooShort
    case invalidCode
}

func applyCoupon(_ code: String) throws -> Int {
    if code.isEmpty {
        throw CouponError.emptyCode
    }

    if code.count < 5 {
        throw CouponError.codeTooShort
    }

    if code == "SAVE10" {
        return 10
    }

    throw CouponError.invalidCode
}

let enteredCode = "SAVE10"

do {
    let discount = try applyCoupon(enteredCode)
    print("Coupon applied. Discount:", discount, "%")
} catch CouponError.emptyCode {
    print("Please enter a coupon code.")
} catch CouponError.codeTooShort {
    print("That coupon code is too short.")
} catch CouponError.invalidCode {
    print("That coupon code is not valid.")
} catch {
    print("Unexpected coupon error:", error)
}
```

Output (tested with Swift 6.4):

```text
Coupon applied. Discount: 10 %
```

This example brings together the main ideas: custom errors, a throwing function, `try`, several specific `catch` clauses, and a final fallback catch.

## 11. Key Points

- `do-catch` handles errors from code marked `throws`.
- You must use `try` when calling a throwing function.
- `catch` can match specific error cases or handle errors generally.
- `try?` turns errors into `nil`, while `try!` crashes if an error is thrown.
- Custom error enums make code easier to read and easier to handle correctly.
- `do-catch` is best when you need explicit, user-friendly, or recoverable error handling.

## 12. Practice Exercise

Create a throwing function named `checkTemperature` that accepts an `Int`. It should throw one error if the temperature is below 0 and another if it is above 35. Then call it inside a `do-catch` block and print a different message for each error.

- Define an error enum with two cases.
- Write the throwing function.
- Use `try` inside a `do` block.
- Add separate `catch` clauses.

**Expected output:** If the temperature is outside the allowed range, print a specific message. Otherwise, print that the temperature is acceptable.

**Hint:** Use an `if` check for values below 0 and another for values above 35, and throw the matching enum case.

```swift
enum TemperatureError: Error {
    case tooCold
    case tooHot
}

func checkTemperature(_ value: Int) throws {
    if value < 0 {
        throw TemperatureError.tooCold
    }

    if value > 35 {
        throw TemperatureError.tooHot
    }
}

do {
    try checkTemperature(40)
    print("Temperature is acceptable.")
} catch TemperatureError.tooCold {
    print("Temperature is below the safe range.")
} catch TemperatureError.tooHot {
    print("Temperature is above the safe range.")
} catch {
    print("Unexpected temperature error:", error)
}
```

Output (tested with Swift 6.4):

```text
Temperature is above the safe range.
```

## 13. Final Summary

Swift `do-catch` blocks give you a clear and safe way to handle operations that can fail. They work with `throws`, `throw`, and `try` to make failure explicit instead of hidden. That explicit design is one of Swift’s strengths because it encourages safer code and clearer APIs.

In practice, the most important habits are to define meaningful error types, use `try` correctly, match specific errors when helpful, and avoid `try!` unless failure is truly impossible. If you are comfortable with `do-catch`, a strong next step is learning Swift `throws` functions in more depth and comparing `do-catch` with `Result` and `try?` for different error-handling situations.
