Learn how to code libraries that your iOS teammates will not frown upon using them. In this chapter: Exceptions!
An android in an avalanche of thrown apples — DALL·E 2
The machine has just dispensed your expresso. You grab your cup, and, suddenly you notice a Macbook Pro with a Swift-logo sticker rushing in your direction. An angry face emerges from behind the laptop:
“It’s crashing!”. Your iOS teammate turns the screen at you, pointing to something that looks like assembly code. It takes a while for you to understand: “Ah! Why didn’t you catch the exception?”. “Why didn’t I catch the exception? Why didn’t I catch the exception? How dare you say that?!”. Fuming, the developer throws — pun intended — the Macbook at you. You are clueless about what just happened.
In order to explain the reason behind the wrath of your Swift colleague, let’s remember something you did last summer: you wrote Java!
import java.io.IOException; public class JavaReader { int read() throws IOException { throw new IOException("There's no data"); } public static void main(String... args) { final JavaReader reader = new JavaReader(); try { int value = reader.read(); } catch (IOException ioe) { System.err.println(ioe.getMessage()); } } }
If we rewrite this code again in pure Kotlin fashion it will be something like this:
import java.io.IOException class KotlinReader { fun read(): Int { throw IOException() } } fun main() { // This compiles fines, although not advisable val value = KotlinReader().run { read() } }
Did you notice the difference? In Java IOException
is a checked exception. That means that you must either handle it with a try-catch
block or declare the exception in the function signature. Kotlin went away with checked exceptions. Let’s bring that to the multiplatform world :
// KOTLIN API CODE import io.ktor.utils.io.errors.IOException class Reader { fun read(): Int { throw IOException("No data to read") } }
// EXPORTED OBJ-C HEADER __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("Reader"))) @interface SharedReader : SharedBase - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); - (int32_t)read __attribute__((swift_name("read()"))); @end
// HEADER "TRANSLATED" TO SWIFT public class Reader : KotlinBase { public init() open func read() -> Int32 }
// SWIFT CLIENT CODE func readData() -> Int32 { let reader = Reader() do { // Warning: No calls to throwing functions occur within 'try' expression let value = try reader.read() return value } catch { // Warning: 'catch' block is unreachable because no errors are thrown in 'do' block return -1 } }
The Xcode’s warnings seem to not make sense at first. But as soon as readData()
is invoked, the iOS app crashes, despite the do-try-catch
block.
Job Offers
Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError. It is considered unexpected and unhandled instead. Program will be terminated. Uncaught Kotlin exception: io.ktor.utils.io.errors.IOException: No data to read at 0 shared 0x10d9debf5 kfun:kotlin.Exception#<init>(kotlin.String?;kotlin.Throwable?){} + 133 (/opt/buildAgent/work/acafc8c59a79cc1/kotlin/kotlin-native/runtime/src/main/kotlin/kotlin/Exceptions.kt:25:63) at 1 shared 0x10db71c57 kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String;kotlin.Throwable?){} + 119 (/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:4:58) at 2 shared 0x10db71cbd kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String){} + 93 (/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:5:50) at 3 shared 0x10d97d4c1 kfun:io.aoriani.kmpapp.Reader#read(){}kotlin.Int + 145 at 4 shared 0x10d981aa7 objc2kotlin_kfun:io.aoriani.kmpapp.Reader#read(){}kotlin.Int + 151 at 5 iosApp 0x10ceefa40 $s6iosApp8readDatas5Int32VyF + 64 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:25:32) at 6 iosApp 0x10ceefd64 $s6iosApp11ContentViewVACycfC + 148 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:4:0) at 7 iosApp 0x10ceef58c $s6iosApp6iOSAppV4bodyQrvgAA11ContentViewVyXEfU_ + 44 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:7:4) at 8 SwiftUI 0x113b75a01 get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA25ComplicationIdiomModifierVGAaBHPxAaBHD1__AfA0cH0HPyHCHCTm + 60924 at 9 iosApp 0x10ceef45c $s6iosApp6iOSAppV4bodyQrvg + 156 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:6:3) at 10 iosApp 0x10ceef838 $s6iosApp6iOSAppV7SwiftUI0B0AadEP4body4BodyQzvgTW + 8 at 11 SwiftUI 0x113284dcf get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 27673
Now do you understand why your colleague was mad at you? Even though there was an attempt to catch the Kotlin Exception, it cannot be captured in Swift with the current code.
Okay, let’s follow the recommendation that is in the crash log, and add a @Throws
annotation in the same way we would do to support Java interoperability.
// KOTLIN API CODE import io.ktor.utils.io.errors.IOException class Reader { @Throws(IOException::class) fun read(): Int { throw IOException("No data to read") } }
// EXPORTED OBJ-C HEADER __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("Reader"))) @interface SharedReader : SharedBase - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); /** * @note This method converts instances of IOException to errors. * Other uncaught Kotlin exceptions are fatal. */ - (int32_t)readAndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("read()"))) __attribute__((swift_error(nonnull_error))); @end
// HEADER "TRANSLATED" TO SWIFT public class Reader : KotlinBase { public init() /** * @note This method converts instances of IOException to errors. * Other uncaught Kotlin exceptions are fatal. */ open func read() throws -> Int32 }
The new Objective-Csignature for our read
method may be confusing at first sight for people who are not used to pointers or have never programmed in C. Let’s write a pseudo-Kotlin implementation of it, with some poetic usage of Kotlin Native types:
// This is a pseudo-code! fun readAndReturnError(error: CPointer<NSError?>?): Int { try { return read() } catch(t: Throwable) { // This is not like real CPointers works, // but I am using like this for didatic reasons error?.rawValue = t.asNSError() } }
The pretend CPointer
above works as a container type, pretty much like Optional, but mutable. If some error happens when the function is called, an instance of
NSError will be placed inside the
error
container. Then the caller can inspect the container. If it contains an NSError
that means the function had failed. That is the convention for Cocoa frameworks — the set of Apple’s libraries like UIKit and Foundation, which are used to build iOS and MacOS apps. If some Cocoa method may fail, its last parameter will be a NSErrror** error
. Objective-C does have exceptions but they are reserved “for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server”.
The good news is that when methods following the Cocoa convention are bridged to Swift, they are transformed into a method that throws the NSError, as we saw above. Kotlin takes advantage of that, exporting Objective-C methods that follow the Cocoa convention. That way, a Kotlin exception can be propagated and caught on the Swift side. Your iOS teammate can then use the extension kotlinExtension
to get the Kotlin exception and downcast it to the expected exceptions:
func readData() -> Int32 { let reader = Reader() do { let value = try reader.read() return value } catch let error as NSError { print("NSError: \(error)") switch error.kotlinException { case let ioException as Ktor_ioIOException: print ("Caught IOException") print (ioException.message ?? "") ioException.printStackTrace() case let illegalStateException as KotlinIllegalStateException: print ("Caught IllegalStateException") print (illegalStateException.message ?? "") default: print ("Caught something") } return -1 } }
The app no longer crashes, we can capture the exception and print a nice log:
NSError: Error Domain=KotlinException Code=0 "No data to read" UserInfo={NSLocalizedDescription=No data to read, KotlinException=io.ktor.utils.io.errors.IOException: No data to read, KotlinExceptionOrigin=} Caught IOException No data to read io.ktor.utils.io.errors.IOException: No data to read at 0 shared 0x1088de635 kfun:kotlin.Exception#<init>(kotlin.String?;kotlin.Throwable?){} + 133 (/opt/buildAgent/work/acafc8c59a79cc1/kotlin/kotlin-native/runtime/src/main/kotlin/kotlin/Exceptions.kt:25:63) at 1 shared 0x108a716d7 kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String;kotlin.Throwable?){} + 119 (/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:4:58) at 2 shared 0x108a7173d kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String){} + 93 (/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:5:50) at 3 shared 0x10887b8c1 kfun:io.aoriani.kmpapp.Reader#read(){}kotlin.Int + 145 at 4 shared 0x1088804ab objc2kotlin_kfun:io.aoriani.kmpapp.Reader#read(){}kotlin.Int + 155 at 5 iosApp 0x107de192b $s6iosApp8readDatas5Int32VyF + 171 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:23:18) at 6 iosApp 0x107de2834 $s6iosApp11ContentViewVACycfC + 148 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:4:0) at 7 iosApp 0x107de140c $s6iosApp6iOSAppV4bodyQrvgAA11ContentViewVyXEfU_ + 44 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:7:4) at 8 SwiftUI 0x10db2ea01 get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA25ComplicationIdiomModifierVGAaBHPxAaBHD1__AfA0cH0HPyHCHCTm + 60924 at 9 iosApp 0x107de12dc $s6iosApp6iOSAppV4bodyQrvg + 156 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:6:3) at 10 iosApp 0x107de16b8 $s6iosApp6iOSAppV7SwiftUI0B0AadEP4body4BodyQzvgTW + 8 at 11 SwiftUI 0x10d23ddcf get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 27673 at 12 SwiftUI 0x10db0d8f2 __swift_memcpy49_8 + 14386 at 13 SwiftUI 0x10d23d498 get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 25314 at 14 SwiftUI 0x10db0daaf __swift_memcpy49_8 + 14831 at 15 SwiftUI 0x10d193ff3 block_destroy_helper.6215 + 63948 at 16 AttributeGraph 0x7ff81fd7a1d6 _ZN2AG5Graph11UpdateStack6updateEv + 536 at 17 AttributeGraph 0x7ff81fd7a9aa _ZN2AG5Graph16update_attributeENS_4data3ptrINS_4NodeEEEj + 442 at 18 AttributeGraph 0x7ff81fd824f4 _ZN2AG5Graph20input_value_ref_slowENS_4data3ptrINS_4NodeEEENS_11AttributeIDEjPK15AGSwiftMetadataRhl + 394 at 19 AttributeGraph 0x7ff81fd999f0 AGGraphGetValue + 217 at 20 SwiftUI 0x10db0d9d8 __swift_memcpy49_8 + 14616 at 21 SwiftUI 0x10db0da9c __swift_memcpy49_8 + 14812 at 22 SwiftUI 0x10d193ff3 block_destroy_helper.6215 + 63948 at 23 AttributeGraph 0x7ff81fd7a1d6 _ZN2AG5Graph11UpdateStack6updateEv + 536 at 24 AttributeGraph 0x7ff81fd7a9aa _ZN2AG5Graph16update_attributeENS_4data3ptrINS_4NodeEEEj + 442 at 25 AttributeGraph 0x7ff81fd824f4 _ZN2AG5Graph20input_value_ref_slowENS_4data3ptrINS_4NodeEEENS_11AttributeIDEjPK15AGSwiftMetadataRhl + 394 at 26 AttributeGraph 0x7ff81fd999f0 AGGraphGetValue + 217 at 27 SwiftUI 0x10db2f887 get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA25ComplicationIdiomModifierVGAaBHPxAaBHD1__AfA0cH0HPyHCHCTm + 64642 at 28 SwiftUI 0x10db2f93b get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA25ComplicationIdiomModifierVGAaBHPxAaBHD1__AfA0cH0HPyHCHCTm + 64822 at 29 SwiftUI 0x10d133475 objectdestroy.35Tm + 23116 at 30 AttributeGraph 0x7ff81fd7a1d6 _ZN2AG5Graph11UpdateStack6updateEv + 536 at 31 AttributeGraph 0x7ff81fd7a9aa _ZN2AG5Graph16update_attributeENS_4data3ptrINS_4NodeEEEj + 442 at 32 AttributeGraph 0x7ff81fd81dbe _ZN2AG5Graph9value_refENS_11AttributeIDEPK15AGSwiftMetadataRh + 122 at 33 AttributeGraph 0x7ff81fd99a35 AGGraphGetValue + 286 at 34 SwiftUI 0x10d23c60a get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 21588 at 35 SwiftUI 0x10e216492 block_destroy_helper.183 + 44405 at 36 SwiftUI 0x10e211b9e block_destroy_helper.183 + 25729 at 37 SwiftUI 0x10e212739 block_destroy_helper.183 + 28700 at 38 UIKitCore 0x108f7d798 +[UIScene _sceneForFBSScene:create:withSession:connectionOptions:] + 1393 at 39 UIKitCore 0x109d60465 -[UIApplication _connectUISceneFromFBSScene:transitionContext:] + 1317 at 40 UIKitCore 0x109d60a3d -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 561 at 41 UIKitCore 0x10970636a -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 349 at 42 FrontBoardServices 0x7ff80549db3a -[FBSScene _callOutQueue_agent_didCreateWithTransitionContext:completion:] + 414 at 43 FrontBoardServices 0x7ff8054cc7b9 __92-[FBSWorkspaceScenesClient createSceneWithIdentity:parameters:transitionContext:completion:]_block_invoke.187 + 101 at 44 FrontBoardServices 0x7ff8054abb89 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 208 at 45 FrontBoardServices 0x7ff8054cc3ae __92-[FBSWorkspaceScenesClient createSceneWithIdentity:parameters:transitionContext:completion:]_block_invoke + 343 at 46 libdispatch.dylib 0x108334f5a _dispatch_client_callout + 7 at 47 libdispatch.dylib 0x1083388d1 _dispatch_block_invoke_direct + 495 at 48 FrontBoardServices 0x7ff8054f2bb7 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 29 at 49 FrontBoardServices 0x7ff8054f2aad -[FBSSerialQueue _targetQueue_performNextIfPossible] + 173 at 50 FrontBoardServices 0x7ff8054f2bdf -[FBSSerialQueue _performNextFromRunLoopSource] + 18 at 51 CoreFoundation 0x7ff800387fe4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 16 at 52 CoreFoundation 0x7ff800387f23 __CFRunLoopDoSource0 + 156 at 53 CoreFoundation 0x7ff800387780 __CFRunLoopDoSources0 + 307 at 54 CoreFoundation 0x7ff800381e22 __CFRunLoopRun + 926 at 55 CoreFoundation 0x7ff8003816a6 CFRunLoopRunSpecific + 559 at 56 GraphicsServices 0x7ff809cb1289 GSEventRunModal + 138 at 57 UIKitCore 0x109d5ead2 -[UIApplication _run] + 993 at 58 UIKitCore 0x109d639ee UIApplicationMain + 122 at 59 SwiftUI 0x10df4c666 __swift_memcpy93_8 + 11935 at 60 SwiftUI 0x10df4c513 __swift_memcpy93_8 + 11596 at 61 SwiftUI 0x10d5b07e8 __swift_memcpy195_8 + 12254 at 62 iosApp 0x107de164d $s6iosApp6iOSAppV5$mainyyFZ + 29 (/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:<unknown>) at 63 iosApp 0x107de16d8 main + 8 at 64 dyld 0x1080292be 0x0 + 4429353662 at 65 ??? 0x1159a652d 0x0 + 4657407277
Moral of the story: Always annotate your Kotlin APIs with @Throws
listing all possible exceptions that could be thrown by your code, so your iOS teammate can catch them.
In this chapter, we learned we must declare exceptions in the Kotlin APIs so the Kotlin Native compiler will generate the code that will allow them to be caught in Swift. See you in the next chapter! Meanwhile, check the other episodes of this series: https://medium.com/@aoriani/list/writing-swiftfriendly-kotlin-multiplatform-apis-c51c2b317fce
References:
- Interoperability with Swift/Objective-C — Errors and exceptions https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions
- ROTOLO, Paolo and LABELLARTE, Anna — Kotlin Multiplatform for Android & iOS library developers: Tips for writing Kotlin Multiplatform Android/iOS libraries https://fosdem.org/2023/schedule/event/kmp_for_android_and_ios_library_developers/
- Apple’s Cocoa Documentation — Using and Creating Error Objects — https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/CreateCustomizeNSError/CreateCustomizeNSError.html
- Swift Evolution — Improved NSError Bridging https://github.com/apple/swift-evolution/blob/main/proposals/0112-nserror-bridging.md
- The Swift Programming Language (5.9 beta) — Error Handling https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling/
- Handling Cocoa Errors in Swift – https://developer.apple.com/documentation/swift/handling-cocoa-errors-in-swift
- Name Translation from C to Swift – https://github.com/apple/swift/blob/main/docs/CToSwiftNameTranslation.md#error-handling
- Introduction to Exception Programming Topics for Cocoa –https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Exceptions.html
This article was previously published on proandroiddev.com