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?
is it possible to swizzle the keyword `throw`?
81 views Asked by Tiny Tim AtThere are 2 answers
On
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 throws, 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)
}
}
No, you can't swizzle
throw. But the Swift runtime has a hook,_swift_WillThrow, that lets you examine anErrorat 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_setWillThrowHandlerfunction to set the_swift_willThrowfunction:Output:
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_willThrowhook directly: