Search code examples
swifterror-handlingthrowswizzling

is it possible to swizzle the keyword `throw`?


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?


Solution

  • 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"))")
        }
    }