Do-Catch: What It Is, Construct and Error Handling in Swift

Author: IT Sectr Published: 2026-05-26 Reading time: 8 min

Do-Catch is a Swift language construct for error handling that catches exceptions thrown via throw and executes the corresponding code block. Unlike traditional try-catch in other languages, Swift requires explicit marking of functions that can throw an error with the throws modifier. According to Swift.org, 2026, Do-Catch is the primary error handling mechanism in Swift, providing type safety.

Key Takeaways

  • Do-Catch is a Swift construct that catches errors from blocks with try and throw.
  • throws is a function marker indicating it can throw an error.
  • try is the keyword for calling a throwing function inside a do-block.
  • catch is the error handling block with type pattern matching.
  • try? and try! are alternative forms that convert errors to nil or trigger a crash.

What is Do-Catch?

Do-Catch is a construct in Swift consisting of a do block, inside which code capable of throwing an error is executed, and one or more catch blocks that handle that error. The do block contains calls to throwing functions marked with try, and catch blocks match the error by type.

Swift implements a model of structured error handling, different from exceptions in Objective-C. Here an error is not an exception with stack unwinding, but a value conforming to the Error protocol. Throwing an error via throw transfers control to the nearest catch block without the overhead of stack unwinding.

A key feature of Swift is explicit handling. The compiler does not allow calling a throwing function without try, do-catch, or an alternative mechanism (try?, try!). This eliminates situations where an error goes unnoticed.

Error Model in Swift: Error Protocol and Throwing Functions

At the core of error handling in Swift is the Error protocol. Any type that conforms to this protocol can be thrown via throw. Errors are typically defined as an enum conforming to Error.

Error Protocol

Error is an empty (marker) protocol. It does not require method implementation, only conformance. The Swift compiler uses Error for static checking: a function with throws can only throw a value conforming to Error.

Throwing Functions

A function marked with throws must be called with try. If a throw occurs inside such a function, control transfers to the calling do-catch. If the error is not handled anywhere in the call chain, it propagates to the upper level.

swift
enum FileError: Error {
    case notFound
    case permissionDenied
    case corrupted(String)
}

func readFile(path: String) throws -> String {
    guard FileManager.default.fileExists(atPath: path) else {
        throw FileError.notFound
    }
    return try String(contentsOfFile: path)
}

Rethrows

rethrows is a modifier for functions that accept a throwing closure as a parameter. If the passed closure does not throw an error, the rethrows function can be called without try. This allows writing generic higher-order functions compatible with both throwing and non-throwing closures.

Do-Catch Syntax: Multiple Catch Blocks

Swift supports multiple catch blocks with type-based error matching. This is similar to when in Kotlin or a type-based switch: the first matching catch executes, and the rest are skipped.

Type Matching

Each catch block can contain a pattern matching a specific error type. Swift checks blocks in order, so more specific types should come before general ones. If no pattern matches, the catch block without a pattern executes.

Handling Multiple Types

Type safety of Do-Catch allows handling each error separately or combining multiple types in one block via comma-separated pattern matching. This eliminates cumbersome if-else chains typical for Objective-C, where error analysis was done through return codes or NSError with domain and code checks.

Swift also supports catch where — matching with an additional condition. For example, you can filter errors by a specific code or text: catch URLError where error.code == .notConnectedToInternet. This reduces the number of catch blocks while maintaining precision.

For debugging throwing functions, Swift provides a distinction between errors and exceptions. A Swift error is a return value that does not require stack unwinding. Objective-C exceptions (@try/@catch) work at the runtime level and are used only for fatal failures. Do-Catch handles errors, not exceptions, making it predictable and performant.

When developing libraries and SDKs, it is important to remember that throwing functions are part of the public contract. The error type is not specified in the signature, so document what errors a function can throw in comments or through Result-like enums. This helps API users write correct catch blocks without looking into the implementation.

In the context of asynchronous programming, Do-Catch with async/await solves the callback hell problem. Previously in Swift, error handling in asynchronous code required nested closures with Error? checking in completion handlers. With async/await and Do-Catch, async throwing code looks like synchronous code, simplifying reading and maintaining complex request chains with error handling at each step.

swift
do {
    let content = try readFile(path: "/data/config.json")
    process(content)
} catch FileError.notFound {
    createDefaultConfig()
} catch FileError.permissionDenied {
    requestAccess()
} catch FileError.corrupted(let detail) {
    logCorruption(detail)
} catch {
    print("Unknown error: \(error)")
}

Try Variations: try, try?, try! and When to Use Them

Swift offers three ways to call throwing functions: try, try?, and try!. Each option solves a specific task and has its limitations.

try — Standard Call

try is used inside a do block, and the error is handled in catch. This is the primary way to call throwing functions. The compiler requires try to be in a context where the error can be caught.

try? — Conversion to Optional

try? converts the result of a throwing function to an Optional. If the function throws an error, try? returns nil, otherwise it returns an Optional with the successful value. Useful for calls where the error can be ignored but a success indicator is needed.

try! — Forced Execution

try! suppresses error handling. If the throwing function throws an error, the app terminates with a runtime error. Use try! only when you are absolutely sure the error is impossible — for example, when loading a bundled resource.

swift
// try? — ignoring the error, getting nil on failure
if let data = try? Data(contentsOf: url) {
    processData(data)
}

// try! — only if the error is guaranteed impossible
let bundled = try! String(contentsOfFile: "Assets/default.txt")

// try — standard option in do-catch
do {
    let result = try performNetworkRequest()
    updateUI(result)
} catch {
    showError(error)
}

Do-Catch in iOS Development: Practical Examples

Do-Catch is actively used in iOS development for working with system APIs. Let us consider typical scenarios: working with the file system, Core Data, and network requests.

File System

Many FileManager methods are throwing. Do-Catch allows properly handling missing files, resource busyness, or insufficient permissions.

Network Requests with Codable

Parsing JSON via JSONDecoder is a throwing operation. Do-Catch catches decoding errors and network failures separately, providing the user with an accurate error message.

swift
struct User: Codable {
    let id: Int
    let name: String
}

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Call with handling
do {
    let user = try await fetchUser(id: 42)
    showUser(user)
} catch let error as URLError {
    showNetworkAlert(error)
} catch let error as DecodingError {
    showParseError(error)
} catch {
    showGenericError(error)
}

Common Do-Catch Mistakes

Developers, especially those transitioning from other languages, often make characteristic mistakes when using Do-Catch. Let us look at the most common ones.

  • Forgotten try — calling a throwing function without try causes a compilation error. Swift does not allow implicitly ignoring errors.
  • Empty catch — a catch block without handling hides the error. Always log or handle the error, even if it seems impossible.
  • Catch without matching — a single generic catch for all errors loses the benefits of type safety. Separate handling by types using multiple blocks.
  • Overuse of try! — try! in production code is a source of unexpected crashes. Use it only for bundled resources or tests.

Frequently Asked Questions

How is Do-Catch in Swift different from try-catch in other languages?

In Swift, an error is a value conforming to the Error protocol, not an exception with stack unwinding. Throwing functions must be explicitly marked with throws, and the compiler requires try when calling — this eliminates unhandled errors at compile time.

Can Do-Catch be used with async/await?

Yes, async functions can also be throws, and do-catch works with them. Calling an async throwing function requires try await inside a do block. This is the standard way to handle errors in asynchronous Swift code.

How to handle an error without do-catch?

There are alternatives: try? converts the error to nil, try! causes a crash on error, and rethrows allows propagating an error from a closure. Do-catch remains the primary way of explicit handling.

Can one function throw different types of errors?

Swift does not type the thrown errors — any throwing function can throw any type conforming to Error. Separate handling through catch matching by specific error types.

Should I wrap all errors in a custom type?

Yes, this is a best practice. Define AppError as an enum conforming to Error and convert system errors to domain errors. This unifies handling and isolates the application layer from system details.

Summary

  • Do-Catch is a Swift construct for error handling with mandatory throws marking on functions and try on calls.
  • Swift errors are values conforming to the Error protocol, not exceptions with stack unwinding, providing zero overhead when no failure occurs.
  • Multiple catch blocks allow handling different error types separately with pattern matching similar to switch.
  • try? converts an error to nil for simple scenarios, try! is a forced call with crash risk, used only for guaranteed successful operations.
  • Do-Catch with async/await is the standard way to handle errors in asynchronous Swift code using try await.
  • Avoid empty catch blocks and overuse of try! — this hides errors and leads to unexpected crashes in production.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also