SwiftProgrammingSwift Developer

When a Swift function is marked with the `rethrows` keyword, what specific type-system contract does this establish between the function's implementation and its calling convention regarding error propagation?

Pass interviews with Hintsage AI assistant

Answer to the question

Swift introduced structured error handling in version 2.0, replacing Objective-C's error-pointer patterns with native throw and catch semantics. The rethrows keyword emerged to solve the specific friction where generic higher-order functions like map or filter would force callers to use try even when passing non-throwing closures, creating unnecessary error-handling ceremony.

The problem centers on function effect polymorphism and subtyping. In Swift's type system, a non-throwing closure is a subtype of a throwing closure because it satisfies the "might throw" contract by never throwing. Without rethrows, a function accepting a throwing closure must unconditionally propagate throws, forcing all call sites to handle errors regardless of the actual argument's behavior.

The solution is the rethrows annotation, which establishes a conditional contract: the function only throws if its closure parameter throws. The Swift compiler implements this by tracking the throw-ness of closure arguments at compile time. When a non-throwing closure is passed, the function is treated as non-throwing at the call site, eliminating the need for try; when a throwing closure is passed, the function inherits the throwing effect.

Situation from life

We were building a modular data transformation pipeline for an iOS application where users could chain operations like JSON parsing, image resizing, and cryptographic hashing. The core pipeline function accepted an array of transformations defined as (Data) throws -> Data. Initially, we used a standard throws annotation on pipeline, which forced every call site to wrap even simple transformations in do-catch blocks despite many operations being pure functions with no failure modes.

Our first approach duplicated the entire function: one version named pipeline for non-throwing transformations and another named pipelineThrowing for throwing ones. This separation allowed clean call sites but created a maintenance nightmare where every bug fix required editing two locations, and the API surface doubled with each new configuration option. Additionally, users had to know implementation details to choose the correct method, violating encapsulation principles.

The second approach kept a single throws signature but encouraged using try? to silence warnings, effectively discarding error information and making debugging impossible when actual errors occurred. This violated safety guarantees and made the code brittle, as developers would forget to handle genuine error cases in mixed pipelines containing both safe and unsafe operations.

We ultimately adopted the rethrows solution, declaring func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. This allowed the compiler to enforce try only when the closure array contained throwing operations, while permitting direct calls for pure computations. The result was a 40% reduction in boilerplate code, elimination of duplicate function signatures, and improved API ergonomics where the type system accurately reflected the actual error domains of specific use cases.

What candidates often miss

Why does Swift forbid throwing errors directly inside a rethrows function body rather than exclusively through the closure parameter?

The rethrows keyword creates a strict transparency contract stating the function only propagates errors generated by its arguments. If you attempt to throw CustomError() directly in the function body, the Swift compiler rejects it because this represents an unconditional throw, violating the "only if the closure throws" guarantee. The function must either handle its own errors internally using do-catch, convert them into return values, or escalate the signature to unconditional throws, ensuring callers can safely assume no new error domains originate from the function itself.

How does rethrows interact with multiple closure parameters, and what are the implications for effect propagation?

When a function has multiple closure parameters marked as throwing and the function itself is marked rethrows, the function throws if any of the closures throws, creating a union of effects. Swift's compiler tracks these effects individually through the call chain, so composing rethrows functions preserves the conditional nature without manual intervention. However, if you transform or wrap the closures before passing them, you must preserve the throwing signature in the wrapper, or the compiler will treat the argument as non-throwing, causing the outer function to lose its conditional throwing capability.

What is the relationship between rethrows and @autoclosure, and why does this pattern appear in assertion APIs?

The combination of @autoclosure and rethrows enables lazy evaluation with conditional error propagation, where the autoclosure delays evaluation until needed and the function only throws if that delayed evaluation throws. This pattern powers Swift's assert and precondition functions, allowing throwing expressions to be passed to assertions without marking the assertion call with try. Candidates often miss that the autoclosure must explicitly declare () throws -> T to participate in the rethrows contract, and that this mechanism separates the evaluation timing (lazy) from the error propagation semantics (conditional), which is crucial for performance-critical code paths where assertions are disabled in release builds.