Swift error handling interview questions, with answers
Swift's error handling looks like exceptions but works differently: every throwing call is marked with try, errors are ordinary values, and there is no unwinding cost hidden behind the syntax. Interviews test whether a candidate can choose between try, try? and try!, model errors well, and clean up reliably.
The answers below cover throwing and catching, the three forms of try, custom errors, defer, the Result type and typed throws, with code compiled with swiftc 6.4. Then take the free Swift diagnostic — ten questions across every Swift topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.How does error handling work in Swift?
In short: Functions marked throws can throw values conforming to Error; callers mark the call with try and handle failures in do/catch.
A function that can fail is declared throws, and it signals failure with throw followed by any value that conforms to the Error protocol, usually an enum case. Every call to it must be written with try, which makes failure points visible when reading the code. Inside a do block, a thrown error jumps to the first catch clause whose pattern matches, and a final catch with no pattern handles everything else, binding the error to the name error. A throwing function can also let errors propagate by being marked throws itself.
enum Net: Error { case offline } func fetch() throws -> String { throw Net.offline } do { print(try fetch()) } catch Net.offline { print("no network") } catch { print("other") } // no network
2.What is the difference between try, try? and try! in Swift?
In short: try propagates or catches the error, try? turns it into nil, and try! stops the program if an error is thrown.
Plain try requires the error to be handled, by a surrounding do/catch or by the enclosing function being throws. try? converts the call into an optional expression: a result becomes a value and an error becomes nil, discarding the error's details. try! asserts that no error will be thrown and unwraps the result; if one is thrown anyway, the program stops with a fatal error. Below, a is simply nil, while the same call with try! stops the program. Use try when the caller needs to know why something failed, try? when only success matters, and try! almost never.
struct Bad: Error {} func f(n: Int) throws -> Int { if n < 0 { throw Bad() } return n } let a = try? f(n: -1) let b = try! f(n: -1) // traps
3.How do you define custom errors in Swift?
In short: Make a type conform to Error, usually an enum whose cases describe each failure and carry details as associated values.
Any type can be thrown once it conforms to the Error protocol, which has no requirements. Enums are the usual choice, since each case names one way an operation can fail, and associated values carry the details, such as the amount still needed below. A catch clause can match a specific case and bind its values. Structs suit errors with many fields, and conforming to LocalizedError adds a user-facing errorDescription. Keeping error types specific lets callers handle each failure differently rather than parsing messages.
enum Pay: Error { case short(need: Int) } do { throw Pay.short(need: 50) } catch Pay.short(let n) { print("need \(n) more") } catch { print(error) } // need 50 more
4.What does defer do in Swift?
In short: A defer block runs when the current scope exits, whether by return, throw or falling off the end, which suits cleanup.
defer schedules a block of code to run just before execution leaves the scope in which the defer appears. It runs on every exit path, so cleanup such as closing a file, releasing a lock or resetting state cannot be skipped by an early return or a thrown error. Writing the cleanup right next to the acquisition keeps the two together. When one scope contains several defer statements, they run in reverse order, the last declared first. A defer cannot itself return or throw out of the scope.
func work() { defer { print("cleanup") } print("working") } work() // working // cleanup
5.What is the Result type in Swift?
In short: Result<Success, Failure> is an enum holding either .success with a value or .failure with an error, for passing outcomes around as values.
Result packages the outcome of an operation as a value, which is useful where throwing is not possible or not convenient, such as storing an outcome or passing it to a completion handler. It is an enum with two cases, success carrying the value and failure carrying an Error, handled with a switch as below. get() converts a Result back into a throwing call, and map and flatMap transform the success value. With async/await, throwing functions have replaced most completion-handler uses of Result.
struct Bad: Error {} func parse(_ s: String) -> Result<Int, Bad> { guard let n = Int(s) else { return .failure(Bad()) } return .success(n) } switch parse("7") { case .success(let n): print(n) case .failure: print("bad") } // 7
6.What are typed throws in Swift?
In short: Since Swift 6, throws(E) declares the one error type a function can throw, so catch blocks receive that type instead of any Error.
A plain throws function can throw any Error, so a catch block gets error as the general any Error type and must cast it to learn more. Swift 6 added typed throws: throws(PErr) states that only PErr can be thrown, the compiler enforces it, and in the catch block error has the type PErr, so it can be compared or switched on directly, as below. Typed throws suit self-contained modules and embedded code; for public APIs that may add failure kinds later, untyped throws remains the more flexible default.
enum PErr: Error { case empty } func first(_ s: String) throws(PErr) -> Character { guard !s.isEmpty else { throw .empty } return s.first! } do { print(try first("")) } catch { print(error == .empty) } // true
How the diagnostic asks it
One question from the Swift bank, exactly as a sitting would show it. The bank has 3 on error handling and 30 across Swift.
What does this Swift code print?
enum E: Error { case bad } func f(_ n: Int) throws -> Int { if n < 0 { throw E.bad } return n * 2 } let a = try? f(3) let b = try? f(-1) print(a ?? 0, b ?? 0)
- 1Optional(6) nil
- 26, then it crashes
- 3It does not compile: try? must be inside do/catch
- 46 0correct
try? evaluates a throwing call and converts the outcome into an optional: a successful result becomes Optional(6), and a thrown error is discarded and becomes nil. The ?? defaults unwrap both, so the output is 6 0. Optional(6) nil is what printing a and b directly would show. A crash is what try! does when an error is thrown. try? needs no do/catch, which is its purpose, although it throws away the reason for the failure, so it suits cases where only success matters.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Swift questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.