Thread
#multiplatform
    e

    Eirik Vale Aase

    9 months ago
    We’re having some difficulties with thrown exceptions from the shared code that we handle in the iOS side. We annotate suspending functions in our domain/data layer with
    Throws(Throwable:class)
    and we log these errors to Crashlytics. Problem is, the error we get (when we cast it as an NSError) has a user info dictionary with a KotlinException entry, and it has a code of 0 and a domain of “KotlinException”. This causes all errors logged to crashlytics to be grouped under the same bucket. The Kotlin throwable has a message which looks something like this (this is formatted for readability, it’s still a string)
    Exception in http request:
        Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."
        UserInfo={
            _kCFStreamErrorCodeKey=50,
            NSUnderlyingError=0x28035a940 {
                Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)"
                UserInfo={
                    _NSURLErrorNWPathKey=unsatisfied (No network route),
                    _kCFStreamErrorCodeKey=50,
                    _kCFStreamErrorDomainKey=1
                }
            },
            _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <595E528B-8F59-4D85-88EE-DD084FC43CE7>.<1>,
            _NSURLErrorRelatedURLSessionTaskErrorKey=(
                "LocalDataTask <595E528B-8F59-4D85-88EE-DD084FC43CE7>.<1>"
            ),
            NSLocalizedDescription=The Internet connection appears to be offline.,
            NSErrorFailingURLStringKey=SOME URL,
            NSErrorFailingURLKey=SOME URL,
            _kCFStreamErrorDomainKey=1
        }
    I could try and parse this string, extract regex matches etc, but I was wondering if there is a better way. It seems like Kotlin packs all of the interesting stuff and concatenates their string representations and just stuffs it into the userInfo entry. I would very much have liked to get the actual error instance. How do people handle this?
    r

    Rick Clephas

    9 months ago
    I am a little confused by the example. As far as I know Kotlin does 2 things: • Convert NSErrors from ObjC to ObjCErrorExceptions in Kotlin • Convert Kotlin Exceptions to NSErrors ◦ Using the
    NSError
    in the
    ObjCErrorException
    ◦ or by creating a
    NSError
    with domain
    KotlinException
    , code
    0
    and a
    KotlinException
    entry in the user info Looking at your example it looks like the
    NSError
    contains a Kotlin
    Exception
    which in turn “contains” a
    NSError
    . Could this be due to how a library is converting NSErrors to Exceptions?
    kpgalligan

    kpgalligan

    9 months ago
    I guess I'd like to clarify. You use
    @Throws
    and then attempt to send handled exceptions to Crashlytics from Swift/Objc code? It's been a while, but I think we tried to make that work a while ago or something similar. Currently we convert
    Throwable
    to a custom error report in Crashlytics directly, which allows you to get symbolicated Kotlin. Better grouping is kind of a TBD, TBH, as I believe Crashlytics handles that differently between hard and handled crashes: https://github.com/touchlab/Kermit/blob/main/kermit-crashlytics/src/darwinMain/kotlin/co/touchlab/kermit/crashlytics/CrashlyticsLogWriter.kt#L55
    I tend to avoid using
    @Throws
    as that seems like less of an iOS thing (although I'm sure plenty would disagree). If a function expects to return an error, make that part of the function call, or let the call "crash". I think I've used
    @Throws
    once in prod code, but pulled it out once we figured out what was actually happening.
    r

    Rick Clephas

    9 months ago
    How are those ExceptionModels currently being grouped/shown in Crashlytics? What Kermit is doing should be the ideal way (I think) as it uses the official APIs from Crashlytics. Though that only works if you catch the Exceptions on the Kotlin side. Once they are passed to Swift (and converted to NSErrors) it gets more complicated.
    kpgalligan

    kpgalligan

    9 months ago
    Once they are passed to Swift (and converted to NSErrors) it gets more complicated.
    Indeed, which is (to some degree) why I don't let them get there 🙂
    If it's an unhandled exception, AKA, something that should crash, I let it crash. That triggers 2 reports: a hard crash that terminates in
    konan::abort()
    and a soft crash that shows the full stack. What I need to dig into more is how grouping happens and if we can improve that (just made a little note: https://github.com/touchlab/Kermit/issues/215).
    In a perfect world, we could figure out how to get a hard crash into one report, but I feel like that would take hacking the crashlytics client (which is open source, but that sounds like a lot of work 😞 )
    This isn't "published" yet. Sort of my brain dump on the problem. https://www.kgalligan.com/crash-reporting-and-kmp/
    I haven't put any thought into the
    @Throws
    method in quite a while, but I suspect it's very possible that there's a much easier/better method that I simply haven't come across yet. I'm emotionally prepared for somebody to be like, "why are you doing all that?! Just do ____", but haven't hit that yet.
    There's also a long overdue "polished" blog post about Kermit and Crashlytics coming. It's confusing because you can only use that integration with static frameworks, and most devs starting out with KMP coming from the Android world aren't aware of the difference between static and dynamic, so explaining everything is non-trivial. I want to add some more docs on how you'd do this all without Kermit, which was/is what CrashKiOS is (https://github.com/touchlab/CrashKiOS/) but those docs are pretty old too 🙂
    Maybe today is the day...
    r

    Rick Clephas

    9 months ago
    Awesome going to give those a read and think about this some more 🙂
    kpgalligan

    kpgalligan

    9 months ago
    As a general SDK rule, though, in my experience it didn't seem like Swift/Objc libraries often exposed checked exceptions at the border. It would not be the first time that we made structural compromises to "get Kotlin to work" in a way that will solve issues, but needing to do a try on every method because we don't have a better way to report issues isn't ideal (if, again, that's why there was a
    @Throws
    , which is a big assumption on my part)
    r

    Rick Clephas

    9 months ago
    I am no Swift expert or anything so I am not really sure what is common there. If I think about Swift and errors I think about completion handlers with an error parameter (something like https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518791-centralmanager)
    (or (for sync code) a
    Result
    object https://developer.apple.com/documentation/swift/result)
    kpgalligan

    kpgalligan

    9 months ago
    Well, yeah. Speaking to something you should have a solid grasp on (the coroutines library), a suspend function should turn into a success and error handler on the iOS side (or some transform to RxSwift or Combine). Obviously
    @Throws
    wouldn't work there. Sync call as a result also makes logical sense to me vs throwing an exception for things you expect to happen. Either Swift's "Result" or something similar that comes out of Kotlin. If it were all Kotlin I'd say a sealed class, although going through Objc/Swift makes that's less expressive, obviously. In general, though, a sync call would return something that contains the error, if there is one. Unexpected exceptions would just cause crashes, unless it was a situation where you absolutely should never crash.
    Technically speaking, though, you could either return a typed error response, or "throw", and that would be up to each developer. I think there's a little risk here of catching everything that falls out of Kotlin when that's maybe not a great idea. So, for example, maybe you have some db code and some networking code in the same chain. Network calls fail, and that's OK. Db calls should always succeed, and if they don't, you're in serious trouble. Passing all of those problems onto the caller isn't generally a good way to go.
    Put another way, adding
    @Throws
    means if the app really should crash, it'll be up to the caller to figure that out, and I think that would be really difficult to do (especially if the caller is not a Kotlin/Native expert, which we can assume they're not). Encoding expected errors in the method response lets you put expected problems in the response, and also let the whole thing fall over if it's really in trouble, but again, situation dependent.
    r

    Rick Clephas

    9 months ago
    Agreed! But the challenge is in how the “this exception should be fatal” exceptions are mapped to Swift/ObjC. IMO
    @Throws
    is a great start as it allows us to define which exceptions should be treated as non-fatal once. On the other hand making those non-fatal exceptions part of the return type feels more Swifty then Kotliny. I think ideally the
    @Throws
    would make that particular function return a
    Result
    or something similar in Swift without changing the Kotlin signature.
    kpgalligan

    kpgalligan

    9 months ago
    @Throws is a great start as it allows us to define which exceptions should be treated as non-fatal once.
    You mean as the annotation param? I guess I hadn't really used it that way. I guess you'd then need to define your own exception type hierarchy if you wanted to make sure the right types got sent. Like you'd want to catch exceptions from your networking code and wrap them in something along the lines of
    KeepGoingException
    so that anything unexpected that hit the Swift/Kotlin border would still cause a crash. Otherwise, it gets tricky to know what's an acceptable set of exceptions by type, or at least it can (for example, I don't know off hand what types Ktor throws. I was under the impression that that could differ by platform). Anyway, I'll have to think about the exception type parameter a bit. In theory, if you had a solid list of what's OK to handle, that would work, but in practice I don't think libraries/sdks are always precise that way (I can't tell you off hand exactly how sqliter or sqldelight throw for db calls, and I feel like I should know that 🙂 )
    r

    Rick Clephas

    9 months ago
    Yeah! But it could indeed become a nightmare to maintain unless there is a good structure in the exception types.
    s

    Sam

    9 months ago
    Philosophically errors in Cocoa programing are recoverable events where exceptions are not. You can see this in ObjC with methods that take an NSError pointer.
    - (NSArray *)doSomethingErrorProneWithObject:(id)obj error:(NSError **)error
    Swift muddies the waters a bit because it adds some syntactic sugar that makes it look similar to a Java exception. Swift would expose that method as the following
    func doSomethingErrorProne(with obj: Any) throws
    And calling it has to be in a
    do/try/catch
    block.
    do {
       try doSomethingErrorProne(with: foo)
    } catch {
        // handle error
    }
    There isn’t an actual way to catch an exception thrown from ObjC in Swift IIRC.
    r

    Rick Clephas

    9 months ago
    Yeah such a difference is also present in Kotlin/Java with
    Exception
    ,
    Error
    ,
    RuntimeException
    . Although I think this is more strict in Swift/ObjC than it is in Kotlin/Java.
    s

    Sam

    9 months ago
    In Swift I think the equivalent unrecoverable exception is to use the
    fatalError
    function.
    r

    Rick Clephas

    7 months ago
    FYI to “answer” the original question. This issue is actually caused by the Ktor logic used to convert NSErrors to Exceptions. They actually store the
    NSError
    as well so it should be possible to retrieve it but it’s not straightforward nor ideal. I have created a feature request to expose the interop to application/library code, but it seems this isn’t something we’ll be seeing in stdlib anytime soon. So in the meantime I have been experimenting with this a little and created a small library (haven’t published it just yet) that could potentially solve this. Would love to hear your feedback about the feature request and the current approach of the library 🙂. https://github.com/rickclephas/NSErrorKt
    kpgalligan

    kpgalligan

    7 months ago
    Interesting. Currently looking at a PR for Sentry support in Kermit, so good timing (although, again, I tend to avoid NSError altogether, but maybe need a revisit).
    r

    Rick Clephas

    7 months ago
    Yeah it’s debatable on where and which errors should be logged best, but I think a key part in any solution will be how libraries such as Ktor handle NSErrors coming from the platform APIs.