NSErrorKt
A Kotlin Multiplatform Library to improve NSError
interop.
WARNING: This is an experiment to try and improve Kotlin's
NSError
interop.
To achieve this the library exposes some Kotlin internals that aren't part of any public API!
Why do we need this?
Kotlin already has Throwable
to NSError
interop for straightforward cases as described in the docs.
Though currently there is no way for application or library code to use this interop directly, meaning applications and libraries need to create their own instead.
Ktor
The Ktor Darwin client does this by wrapping a NSError
in a custom Exception
:
@OptIn(UnsafeNumber::class)
internal fun handleNSError(requestData: HttpRequestData, error: NSError): Throwable = when (error.code) {
NSURLErrorTimedOut -> SocketTimeoutException(requestData)
else -> DarwinHttpRequestException(error)
}
Which is a great solution for your Kotlin code as it allows you to access the wrapped NSError
.
However once these Exception
s reach Swift/ObjC, Kotlin will convert them to a NSError
.
Since Kotlin doesn't know this Exception
is a wrapped NSError
it will wrap the Exception
in a NSError
again.
This results in hard to log errors like this one.
KMP-NativeCoroutines
KMP-NativeCoroutines has a similar problem where it needs to convert Exception
s to NSError
s:
@OptIn(UnsafeNumber::class)
internal fun Throwable.asNSError(): NSError {
val userInfo = mutableMapOf<Any?, Any>()
userInfo["KotlinException"] = this.freeze()
val message = message
if (message != null) {
userInfo[NSLocalizedDescriptionKey] = message
}
return NSError.errorWithDomain("KotlinException", 0.convert(), userInfo)
}
It produces similar NSError
s to the once Kotlin creates, but it doesn't unwrap an already wrapped NSError
.
And in case such an NSError
reaches Kotlin again it will be wrapped in an Exception
instead of being unwrapped.
So depending on your code this might result in hard to log errors as well.
Usage
To solve these issues this library exposes the Kotlin NSError
interop logic to your application and library code.
Consisting of 3 extension functions:
and 2 extension properties:
asNSError
/**
* Converts `this` [Throwable] to a [NSError].
*
* If `this` [Throwable] represents a [NSError], the original [NSError] is returned.
* For other [Throwable]s a `KotlinException` [NSError] is returned:
* ```
* NSError.errorWithDomain("KotlinException", 0, mapOf(
* "KotlinException" to this,
* NSLocalizedDescriptionKey to this.message
* ))
* ```
*
* @see throwAsNSError
* @see asThrowable
*/
fun Throwable.asNSError(): NSError
throwAsNSError
/**
* Tries to convert `this` [Throwable] to a [NSError].
*
* If `this` [Throwable] is an instance of one of the [exceptionClasses] or their subclasses,
* it is converted to a [NSError] in the same way [asNSError] would.
*
* Other [Throwable]s are considered unhandled and will cause program termination
* in the same way a [Throws] annotated function would.
*
* @see asNSError
* @see asThrowable
* @see Throws
*/
fun Throwable.throwAsNSError(vararg exceptionClasses: KClass<out Throwable>): NSError
asThrowable
/**
* Converts `this` [NSError] to a [Throwable].
*
* If `this` [NSError] represents a [Throwable], the original [Throwable] is returned.
* For other [NSError]s an [ObjCErrorException] will be returned.
*
* @see asNSError
*/
fun NSError.asThrowable(): Throwable
isNSError
/**
* Indicates if `this` [Throwable] represents a [NSError].
*/
val Throwable.isNSError: Boolean
isThrowable
/**
* Indicates if `this` [NSError] represents a [Throwable].
*/
val NSError.isThrowable: Boolean