1

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?

Ken White
  • 123,280
  • 14
  • 225
  • 444
Tiny Tim
  • 207
  • 1
  • 6
  • 3
    Swizzling 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 Answers2

3

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)
    }
}
Sweeper
  • 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
1

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"))")
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848