Swift do-catch Blocks: Error Handling Syntax and Examples

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.

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.

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)
}

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:

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 or Core Idea

Mark a function as throwing

A function that can fail must be marked with throws.

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.

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

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

Catch specific errors

You can match particular error cases for more precise handling.

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

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.

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")
}

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.

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)
}

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.

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.")
}

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.

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)
}

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

5. Practical Use Cases

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.

enum FileError: Error {
case missing
}

func loadFile() throws {
throw FileError.missing
}

loadFile()

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

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.

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

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

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

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

greet()

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.

enum NetworkError: Error {
case offline
case timeout
}

func fetchProfile() throws {
throw NetworkError.offline
}

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

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

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.

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.

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.

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.

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

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.

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

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.

FormWhat it doesUse when
do { try ... } catchHandles errors explicitlyYou need the reason for failure or custom recovery
try?Converts errors into nilYou only care whether a value was returned
try!Crashes if an error is thrownYou 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.

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)
}

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

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.

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.

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)
}

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.