Swiftyn LogoSwiftyn
LearnInterview PrepRoadmapsArchitect Profile
Swift LanguageSwiftUIUIKitiOS ConceptsmacOS

Swift Language Topics

Introduction to SwiftVariables and ConstantsData TypesType InferenceOperatorsStrings and CharactersBooleansTuplesIf Else StatementsSwitch StatementsGuard StatementsLoopsBreak and ContinueArraysDictionariesSetsCollection OperationsFunctionsFunction ParametersReturn TypesInout ParametersVariadic ParametersClosuresTrailing ClosuresEscaping ClosuresAuto ClosuresCapture ListsOptionalsOptional BindingNil CoalescingOptional ChainingImplicitly Unwrapped OptionalsStructuresClassesPropertiesComputed PropertiesProperty ObserversMethodsInitializationDeinitializationInheritancePolymorphismEncapsulationAccess ControlStatic vs Class MethodsProtocolsProtocol ExtensionsProtocol CompositionAssociated TypesExtensionsGenericsGeneric ConstraintsOpaque TypesExistential TypesType CastingAny and AnyObjectNested TypesSubscriptsKeyPathsThrowing FunctionsDo Try CatchCustom ErrorsResult TypeARCStrong ReferencesWeak ReferencesUnowned ReferencesRetain CyclesMemory Leaks
Browse Swift Language Topics
Introduction to SwiftVariables and ConstantsData TypesType InferenceOperatorsStrings and CharactersBooleansTuplesIf Else StatementsSwitch StatementsGuard StatementsLoopsBreak and ContinueArraysDictionariesSetsCollection OperationsFunctionsFunction ParametersReturn TypesInout ParametersVariadic ParametersClosuresTrailing ClosuresEscaping ClosuresAuto ClosuresCapture ListsOptionalsOptional BindingNil CoalescingOptional ChainingImplicitly Unwrapped OptionalsStructuresClassesPropertiesComputed PropertiesProperty ObserversMethodsInitializationDeinitializationInheritancePolymorphismEncapsulationAccess ControlStatic vs Class MethodsProtocolsProtocol ExtensionsProtocol CompositionAssociated TypesExtensionsGenericsGeneric ConstraintsOpaque TypesExistential TypesType CastingAny and AnyObjectNested TypesSubscriptsKeyPathsThrowing FunctionsDo Try CatchCustom ErrorsResult TypeARCStrong ReferencesWeak ReferencesUnowned ReferencesRetain CyclesMemory Leaks
Swiftyn Logo

Swiftyn

The go-to platform for Apple developers. Swift, SwiftUI, and beyond.

Questions? Email us at support@swe180.com

Categories

  • SwiftUI
  • Swift Language
  • Xcode
  • visionOS

Our Products

  • SWE180
  • One Percent Engineer

Resources

  • About
  • RSS Feed
  • Apple Developer

© 2026 Swiftyn. All rights reserved.

Privacy PolicyTerms of Service

Swiftyn is the premier learning platform and developer resource for mastering the Apple ecosystem. Whether you are an aspiring iOS developer looking to learn Swift 6, a macOS engineer diving into advanced system architecture, or an XR pioneer building the future with visionOS, our beautifully crafted tutorials, roadmaps, and interview prep guides have you covered. Built by Apple developers, for Apple developers.

Swift Language12 min read

Mastering Throwing Functions in Swift for Robust Error Handling

Throwing functions are a fundamental part of Swift's robust error handling mechanism, allowing you to indicate that a function, method, or initializer might encounter an error during its execution. By embracing throwing functions, you can write expressive and safe code that explicitly acknowledges potential failure points. This guide will walk you through the core concepts and advanced patterns.

Understanding Error Handling in Swift

Error handling in Swift is a first-class citizen, designed to allow you to respond to error conditions that your program might encounter during execution. Unlike languages that rely heavily on nil returns or unchecked exceptions, Swift uses a system that forces you to acknowledge and handle potential errors. This system is built around the Error protocol, custom error types, and throwing functions.

At its core, error handling in Swift involves four keywords:

  • throws: Denotes that a function, method, or initializer can throw an error.
  • try: Used when calling a throwing function.
  • do-catch: A block for handling errors thrown by try expressions.
  • rethrows: Indicates that a function only throws an error if one of its function parameters throws an error.

By explicitly dealing with errors, you can create more reliable and predictable applications. This approach helps prevent unexpected crashes and provides clear pathways for problem resolution within your code.

Defining and Throwing Custom Errors

Before you can throw an error, you need to define what kinds of errors your function might produce. In Swift, errors are represented by types that conform to the Error protocol. This protocol has no requirements, so you can easily create custom error types using enums, structs, or classes. Enums are often the preferred choice for error types because they allow you to define a finite set of distinct error cases, optionally with associated values to provide more context.

Let's say you're building a network service. You might define errors related to network connectivity, server responses, or data parsing.

swift
enum NetworkError: Error, LocalizedError {
    case invalidURL
    case networkUnavailable
    case serverError(statusCode: Int)
    case decodingFailed(description: String)
    case unknown(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "The provided URL is not valid."
        case .networkUnavailable:
            return "It appears you are offline. Please check your network connection."
        case .serverError(let statusCode):
            return "The server returned an error with status code \(statusCode)."
        case .decodingFailed(let description):
            return "Failed to decode data: \(description)"
        case .unknown(let error):
            return "An unknown error occurred: \(error.localizedDescription)"
        }
    }
}

Once you have defined your error types, you can use the throw statement to throw an error from within a throwing function. When an error is thrown, the execution of the current scope immediately stops, and control is transferred to the nearest catch block that can handle the specific error type.

Consider a function that fetches data from a URL. It might throw various NetworkError cases:

swift
func fetchData(from urlString: String) throws -> Data {
    guard let url = URL(string: urlString) else {
        throw NetworkError.invalidURL
    }

    // Simulate a network request that might fail
    // In a real app, this would involve URLSession
    if urlString.contains("badurl") {
        throw NetworkError.networkUnavailable
    } else if urlString.contains("500") {
        throw NetworkError.serverError(statusCode: 500)
    }

    print("Successfully fetched data from \(url.absoluteString)")
    return Data()
}
swift
enum DataProcessingError: Error {
    case invalidInput
    case insufficientPermissions
    case transformationFailed(reason: String)
}

func processData(input: String) throws -> String {
    guard !input.isEmpty else {
        throw DataProcessingError.invalidInput
    }

    if input.contains("admin") {
        throw DataProcessingError.insufficientPermissions
    }

    // Simulate data transformation that might fail
    if input.count > 10 {
        throw DataProcessingError.transformationFailed(reason: "Input too long")
    }

    let processed = input.uppercased()
    return processed
}

Calling Throwing Functions with try, try?, and try!

When you call a function that is marked with throws, you must use one of the try operators. Swift forces you to acknowledge that the function might throw an error and handle it appropriately. There are three main ways to call a throwing function:

do-catch Blocks for Error Handling

The most common and recommended way to handle errors is within a do-catch statement. This allows you to attempt to execute code that might throw an error and then gracefully handle any errors that are thrown. You can specify multiple catch blocks to handle different error types, similar to switch statements.

swift
do {
    let result = try processData(input: "hello")
    print("Processed data: \(result)")

    let anotherResult = try fetchData(from: "https://example.com/data")
    print("Fetched data size: \(anotherResult.count)")

    // This will throw and be caught by the NetworkError catch block
    _ = try fetchData(from: "https://example.com/badurl")
} catch DataProcessingError.invalidInput {
    print("Error: Input for data processing is invalid.")
} catch NetworkError.networkUnavailable {
    print("Network error: Device is offline.")
} catch NetworkError.serverError(let statusCode) {
    print("Server error: Received status code \(statusCode).")
} catch NetworkError.unknown(let error) {
    print("An unknown network error occurred: \(error.localizedDescription)")
} catch {
    // A catch-all block for any other errors
    print("An unexpected error occurred: \(error.localizedDescription)")
}

try? for Optional Return Values

When you don't need to handle every specific error, but simply want to know if an operation succeeded or failed, you can use try?. This operator converts a throwing function into a non-throwing function that returns an optional value. If the function throws an error, try? returns nil. Otherwise, it returns an optional containing the value the function would have returned.

swift
let processedDataOptional = try? processData(input: "short")
if let data = processedDataOptional {
    print("Safely processed data: \(data)")
} else {
    print("Data processing failed silently.")
}

let potentiallyBadFetch = try? fetchData(from: "https://example.com/badurl")
if potentiallyBadFetch == nil {
    print("Fetch operation failed or returned nil.")
}

try! for Forced Unwrapping (Use with Caution)

try! is used when you are absolutely certain that a throwing function will never throw an error at runtime. If the function does throw an error, your program will crash. This is similar to force-unwrapping an optional (someValue!). You should use try! very sparingly, typically only in situations where you have guaranteed preconditions (e.g., loading a known-good resource from your app bundle) or during testing.

swift
// Example: Loading a resource from the app bundle that you know exists
let path = Bundle.main.path(forResource: "config", ofType: "json")!
let configURL = URL(fileURLWithPath: path)
alet configData = try! Data(contentsOf: configURL)
print("Config data loaded: \(configData.count) bytes")

// WARNING: This will cause a runtime crash if fetchData throws
// let crashProneData = try! fetchData(from: "https://example.com/badurl")

The rethrows Keyword for Higher-Order Functions

The rethrows keyword is a powerful feature for higher-order functions – functions that take other functions as arguments. A function marked with rethrows indicates that it only throws an error if one of its function parameters throws an error. If none of its function parameters throw, the rethrows function itself won't throw any error.

This is particularly useful for functions like map, filter, or reduce that operate on collections and apply a closure. If the closure you pass to map is throwing, then the map function itself should be able to propagate that error. By using rethrows, you avoid having to mark the map function as unconditionally throws, which would make it less flexible when its closure parameter is non-throwing.

Let's consider a custom processAll function that applies a transformation to an array of items:

swift
enum TransformationError: Error {
    case cannotTransformNegativeNumber
}

// A throwing transformation closure
func transform(number: Int) throws -> Int {
    guard number >= 0 else {
        throw TransformationError.cannotTransformNegativeNumber
    }
    return number * 2
}

// A non-throwing transformation closure
func safeTransform(number: Int) -> Int {
    return number + 1
}

// 'rethrows' allows 'processAll' to throw only if 'transformClosure' throws.
func processAll<T>(items: [T], transformClosure: (T) throws -> T) rethrows -> [T] {
    var processedItems: [T] = []
    for item in items {
        processedItems.append(try transformClosure(item))
    }
    return processedItems
}

// Example 1: Using with a throwing closure
do {
    let numbers = [1, 2, 3, -4, 5]
    let doubledNumbers = try processAll(items: numbers, transformClosure: transform)
    print("Doubled positive numbers: \(doubledNumbers)")
} catch TransformationError.cannotTransformNegativeNumber {
    print("Error: Tried to transform a negative number.")
} catch {
    print("An unexpected error occurred: \(error)")
}

// Example 2: Using with a non-throwing closure
let safeNumbers = [10, 20, 30]
let incrementedNumbers = processAll(items: safeNumbers, transformClosure: safeTransform)
print("Incremented numbers: \(incrementedNumbers)") // No 'try' needed here!

Notice that when processAll is called with safeTransform (which is non-throwing), you don't need to use try. This demonstrates the flexibility rethrows provides.

swift
enum MyOperationError: Error {
    case invalidOperation
}

func executeIfValid(operation: () throws -> Void) rethrows {
    // Pre-check for validity (non-throwing)
    print("Performing pre-check...")

    // The 'try'在这里 is conditional based on 'operation' throwing
    try operation()

    print("Operation completed successfully.")
}

doSweepstakes(chance: 0.1)

// Example 1: Calling with a throwing closure
do {
    try executeIfValid {
        let randomNumber = Int.random(in: 1...10)
        if randomNumber % 2 != 0 {
            throw MyOperationError.invalidOperation
        }
        print("\tInner operation succeeded with \(randomNumber).")
    }
} catch MyOperationError.invalidOperation {
    print("Error caught: Invalid operation due to odd number.")
} catch {
    print("An unexpected error occurred: \(error)")
}

print("\n")

// Example 2: Calling with a non-throwing closure
executeIfValid {
    print("\tInner operation succeeded (non-throwing).")
}

Best Practices for Error Handling with Throwing Functions

Effective error handling goes beyond just knowing the syntax; it involves thoughtful design and consistent application. Here are some best practices:

  1. Define Custom, Descriptive Error Types: Use enums that conform to Error to represent distinct error conditions. Add associated values for extra context (e.g., statusCode for network errors). Make them LocalizedError for user-facing messages.
  2. Be Specific with catch Blocks: Handle specific error types or error domains first, then use a general catch block for any unhandled errors. This allows for precise error recovery.
  3. Use do efficiently: Group related throwing calls within a single do block. If any call throws an error, the rest of the do block is skipped, and control jumps to the catch block.
  4. Avoid try!: Only use try! when you are absolutely certain an error will not occur, such as testing or when loading known-good, static bundle resources. Overuse leads to brittle code and crashes.
  5. Prefer try? when appropriate: If you only need to know if an operation succeeded or failed and don't require specific error details, try? provides a concise way to convert a throwing result into an optional.
  6. Handle Errors at the Right Level: Decide where in your application's architecture it's most appropriate to handle an error. Sometimes an error should propagate up the call stack to a UI layer for user notification, while other times it should be handled internally for retry logic or logging.
  7. Consider defer for Cleanup: The defer statement is invaluable for ensuring resources are cleaned up (e.g., closing files, releasing locks) regardless of whether a function exits normally or by throwing an error.
swift
func processFile(path: String) throws {
    let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer {
        try? fileHandle.close()
        print("File handle closed.")
    }

    // Perform operations that might throw
    // ...
    print("File processed successfully.")
}

By following these guidelines, you can write more resilient, maintainable, and developer-friendly code that embraces Swift's powerful error-handling capabilities. Remember, well-handled errors contribute significantly to a great user experience on iOS and macOS.

Ignoring Potential Failures

Mastering Swift Throwing Functions

THE MYTH or PROBLEM: Ignoring Potential Failures

Many developers, especially when new to Swift, might be tempted to ignore potential errors or use `try!` excessively, assuming a function 'will never fail'. This leads to brittle code that crashes unexpectedly in production when an unhandled error occurs.

swift
func unsafeLoadConfig() -> Data {
    let url = URL(string: "file:///bad/path/config.json")!
    return try! Data(contentsOf: url) // ⚠️ CRASHES if file doesn't exist
}

WHAT HAPPENS INTERNALLY? Error Propagation

When an error is `throw`n, Swift unwinds the call stack, looking for the nearest `do-catch` block capable of handling that error type. If no handler is found, the program terminates.

App Execution
Function A (calls B)
Function B (calls C)
Function C (throws Error)
1

1. Function Throws

A `throws` function encounters an error condition and uses `throw SomeError`.

2

2. Call Stack Unwinds

Execution halts, and the Swift runtime searches for an error handler up the call stack.

3

3. `do-catch` Match

If a `do-catch` block catches the error type, execution transfers to the appropriate `catch` block.

4

4. Error Handled / Propagated

Program continues from the `catch` block; or, if no handler found, the error propagates further or crashes.

Visualized execution hierarchy.

Powerful Guarantees

Compile-Time Safety

Swift's error handling system requires explicit handling of throwing functions, ensuring you don't accidentally ignore potential errors.

Clear Failure Paths

By using custom `Error` types, you provide semantic meaning to failures, making code easier to debug and maintain.

Resource Cleanup (`defer`)

`defer` statements guarantee cleanup code runs whether a function exits normally or abnormally (by throwing an error).

REAL PRODUCTION EXAMPLE: A Fragile Network Layer

A common anti-pattern: using `try!` or `try?` blindly in a network layer without specific error handling. This could lead to silent failures (`try?`) or app crashes (`try!`) if the network is down or the server returns unexpected data, making debugging and user experience poor.

Impact / Results
Unpredictable app crashes
Poor user experience (no feedback on failures)
Difficult to debug network issues
THE FIX or SOLUTION: Robust Network Request with `do-catch`
swift
enum APIError: Error {
    case invalidURL
    case network(Error)
    case decoding(Error)
    case server(statusCode: Int)
}

func performRequest(urlString: String, completion: @escaping (Result<Data, APIError>) -> Void) {
    guard let url = URL(string: urlString) else {
        completion(.failure(.invalidURL))
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(.network(error)))
            return
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            completion(.failure(.network(URLError(.badServerResponse))))
            return
        }

        guard (200...299).contains(httpResponse.statusCode) else {
            completion(.failure(.server(statusCode: httpResponse.statusCode)))
            return
        }

        guard let data = data else {
            completion(.failure(.decoding(URLError(.zeroByteResource))))
            return
        }

        // In a throwing function structure, it would look more like this:
        // func fetchData() throws -> Data {
        //    ... fetch logic ...
        //    if let data = data { return data } else { throw APIError.decoding(...) }
        // }
        // Then you'd call it with do-catch:
        // do { let data = try fetchData() } catch APIError.network { ... }
        // For async/await, it's even cleaner:
        // func fetchData() async throws -> Data { ... }
        // do { let data = try await fetchData() } catch { ... }

        completion(.success(data))
    }.resume()
}

INTERVIEW PERSPECTIVE

Common Question

“Explain Swift's error handling mechanism and when you would use `throws`, `try?`, and `try!`.”

Strong Answer

A strong answer would explain that Swift's error handling is for recoverable errors, using types conforming to `Error`. `throws` marks functions that can fail. `try` is used with `do-catch` for explicit, detailed error recovery. `try?` converts throwing to optional results for when error details aren't needed. `try!` is for forced unwrapping when absolute certainty of success exists, heavily discouraged due to crash risks.

Interviewers Expect you to understand:
  • Clear definition of `Error` protocol
  • `do-catch` as primary mechanism
  • Correct usage scenarios for `try?` and `try!` (with caveats)
  • Discussion of `rethrows` for advanced cases
KEY TAKEAWAY

Always anticipate and explicitly handle potential errors in your Swift code using `do-catch` with custom `Error` types. Reserve `try?` for graceful failures and `try!` only when a crash is genuinely acceptable.

Common Interview Questions

What is the difference between `throws` and `rethrows`?

`throws` indicates that a function *can directly* throw an error itself. `rethrows` indicates that a higher-order function *only throws an error if one of its function parameters throws an error*. If all its function parameters are non-throwing, then a `rethrows` function can be called without `try`.

When should I use `try?` versus `do-catch`?

Use `do-catch` when you need to specifically handle different types of errors, provide detailed error messages to the user, or implement recovery logic. Use `try?` when you only care if an operation failed or succeeded, and a `nil` result is sufficient to indicate failure (e.g., attempting a conversion that might not be possible, but doesn't warrant detailed error handling).

Can I throw any type in Swift?

No, only types that conform to the `Error` protocol can be thrown. `Error` is a light-weight protocol with no requirements, making it easy to create custom error types using enums, structs, or classes. Enums are generally preferred for their expressiveness.

What happens if a throwing function doesn't have a `catch` block?

If a throwing function is called with `try` within a `do` block, and an error is thrown but no `catch` block matches the error type, the error will propagate up to the nearest enclosing scope that can handle it (either another `do-catch` or a throwing function). If it reaches the top-level of an execution context (e.g., the top-level of an app's `main.swift` or a Playground), an unhandled error will cause a runtime crash.

Is error handling in Swift similar to exceptions in other languages?

While Swift's error handling (`throws`, `try`, `catch`) shares conceptual similarities with exceptions in languages like Java or C#, there's a key distinction: Swift's system is designed for *recoverable* errors, where your program can often proceed after handling the error. This is different from Objective-C's exceptions, which are typically used for *programmer errors* or unexpected, irrecoverable conditions. Swift's system forces explicit error handling, making your code safer and more predictable.

#Swift#Error Handling#Throwing Functions#try catch#rethrows#Swift 5.0+