We’re having some difficulties with thrown excepti...
# multiplatform
e
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)
Copy code
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?
👀 3
r
I am a little confused by the example. As far as I know Kotlin does 2 things: • Convert `NSError`s from ObjC to `ObjCErrorException`s in Kotlin • Convert Kotlin `Exception`s to `NSError`s ◦ 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 `NSError`s to `Exception`s?
k
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
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.
k
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/
👀 1
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
Awesome going to give those a read and think about this some more 🙂
k
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
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)
k
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
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.
k
@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
Yeah! But it could indeed become a nightmare to maintain unless there is a good structure in the exception types.
s
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.
Copy code
- (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
Copy code
func doSomethingErrorProne(with obj: Any) throws
And calling it has to be in a
do/try/catch
block.
Copy code
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
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
In Swift I think the equivalent unrecoverable exception is to use the
fatalError
function.
r
FYI to “answer” the original question. This issue is actually caused by the Ktor logic used to convert `NSError`s to `Exception`s. 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
🔥 2
👀 2
k
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
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.
599 Views