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.
- 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.
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:
- 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 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
- 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.
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
- 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.
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
- 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.
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.