I was thinking about error handling and I learned about swizzling recently. Swizzling is certainly a tool which shouldn't be used too often, and I think I understand that, but it made me wonder. If whenever an error is thrown, if I wanted to capture the thrown error. Is there a way I could use swizzling or some such in order to intercept the error and log it somewhere without interrupting the flow of the app? I was thinking about possibly swizzling the throws
keyword, but that might not work. What tools would be used for this kind of thing?
-
3Swizzling only works by swapping the method implementations that correspond to particular selectors within the Objective C runtime. Anything that isn't a method call (and not just any method call, but a dynamically dispatched method call, so a call to a `dynamic @objc func`) can't be swizzled. `throw` doesn't fall in that category. – Alexander Feb 18 '23 at 02:53
2 Answers
No you cannot. Many compiler checks depend on the fact that throw
"interrupts the flow of the app". For example, if some path of the code throw
s, then that path doesn't need to return
:
func foo(x: Bool) throws -> Int {
if x {
throw someError
} else {
return 1
}
}
Now if throw someError
did not "interrupt the flow of the app", what would bar
print in the following code?
func bar() throws {
let x = try foo(x: true)
print(x)
}
Another example is guard
:
guard let value = somethingThatMayBeNil else {
throw someError
}
print(value.nowICanSafelyAccessThis)
If throw someError
above didn't actually "interrupt the flow of the app" and stop running the rest of the method, something really bad is going to happen at print(value.nowICanSafelyAccessThis)
, because somethingThatMayBeNil
is nil, and I'm not even sure value
even exists.
The whole point is that throw
would unwind the call stack to a point where the error can be caught, and that the code that depends on there not being an error is not executed.
If you want to "capture" the error in some way, you can use a Result
. The first example can be turned into:
func foo(x: Bool) -> Result<Int, Error> {
if x {
return Result.failure(someError)
} else {
return Result.success(1)
}
}
func bar() {
let x = foo(x: true)
// now this will print "failure(...)"
print(x)
// and x is of type Result<Int, Error>, rather than Int
switch x {
case .failure(let e):
// log the error e here...
case .success(let theInt):
// do something with theInt...
}
}
You can also use init(catching:)
to wrap any throwing call into a Result
. Suppose if you can't change foo
, then you can do this in bar
instead:
func bar() {
let x = Result { try foo(x: true) }
...
The second example can be turned into:
guard let value = somethingThatMayBeNil else {
// assuming the return type is changed appropriately
return Result.failure(someError)
}
print(value.nowICanSafelyAccessThis)
Note that this will still "interrupt the flow of the app", as in the print
is still not executed if somethingThatMayBeNil
is nil. There is nothing you can do about that.
You could also add a convenient factory method for logging:
extension Result {
static func failureAndLog(_ error: Failure) -> Result {
// do your logging here...
return .failure(error)
}
}

- 213,210
- 22
- 193
- 313
-
This is really well explained. It seemed like `Result` was most often used in conjunction with escaping closures, and were largely replaced with structured concurrency wherein we now had `async throws`. Seems like there's some value in continued use of `Result`. Thanks Sweeper! – Tiny Tim Feb 18 '23 at 06:20
No, you can't swizzle throw
. But the Swift runtime has a hook, _swift_WillThrow
, that lets you examine an Error
at the moment it's about to be thrown. This hook is not a stable API and could be changed or removed in future versions of Swift.
If you're using Swift 5.8, which is included in Xcode 14.3 (in beta release now), you can use the _swift_setWillThrowHandler
function to set the _swift_willThrow
function:
@_silgen_name("_swift_setWillThrowHandler")
func setWillThrowHandler(
_ handler: (@convention(c) (UnsafeRawPointer) -> Void)?
)
var errors: [String] = []
func tryItOut() {
setWillThrowHandler {
let error = unsafeBitCast($0, to: Error.self)
let callStack = Thread.callStackSymbols.joined(separator: "\n")
errors.append("""
\(error)
\(callStack)
""")
}
do {
throw MyError()
} catch {
print("caught \(error)")
print("errors = \(errors.joined(separator: "\n\n"))")
}
}
Output:
caught MyError()
errors = MyError()
0 iPhoneStudy 0x0000000102a97d9c $s11iPhoneStudy8tryItOutyyFySVcfU_ + 252
1 iPhoneStudy 0x0000000102a97ff0 $s11iPhoneStudy8tryItOutyyFySVcfU_To + 12
2 libswiftCore.dylib 0x000000018c2f4ee0 swift_willThrow + 56
3 iPhoneStudy 0x0000000102a978f8 $s11iPhoneStudy8tryItOutyyF + 160
4 iPhoneStudy 0x0000000102a99740 $s11iPhoneStudy6MyMainV4mainyyFZ + 28
5 iPhoneStudy 0x0000000102a997d0 $s11iPhoneStudy6MyMainV5$mainyyFZ + 12
6 iPhoneStudy 0x0000000102a99f48 main + 12
7 dyld 0x0000000102d15514 start_sim + 20
8 ??? 0x0000000102e11e50 0x0 + 4343275088
9 ??? 0x9f43000000000000 0x0 + 11476016275470155776
If you're using an older Swift (but at least Swift 5.2 I think, which was in Xcode 11.4), you have to access the _swift_willThrow
hook directly:
var swift_willThrow: UnsafeMutablePointer<(@convention(c) (UnsafeRawPointer) -> Void)?> {
get {
dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_swift_willThrow")!
.assumingMemoryBound(to: (@convention(c) (UnsafeRawPointer) -> Void)?.self)
}
}
var errors: [String] = []
func tryItOut() {
swift_willThrow.pointee = {
let error = unsafeBitCast($0, to: Error.self)
let callStack = Thread.callStackSymbols.joined(separator: "\n")
errors.append("""
\(error)
\(callStack)
""")
}
do {
throw MyError()
} catch {
print("caught \(error)")
print("errors = \(errors.joined(separator: "\n\n"))")
}
}

- 375,296
- 67
- 796
- 848