Blog Infos
Author
Published
Topics
Published

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

Kotlin Multiplatform Mobile (KMM) is awesome for us Android Developers. Writing multiplatform code with it doesn’t diverge much from our usual routine, and now with Compose Multiplaform, we can write an entire iOS app without…
Watch Video

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software Engineer
Walmart

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software E ...
Walmart

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software Engine ...
Walmart

Jobs

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:

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources…
READ MORE
blog
There are already so many app-level architecture and presentation layer patterns (MVC/MVP/MVVM/MVI/MVwhatever) that exist…
READ MORE
blog
This article will continue the story about the multi-platform implementation of the in-app review.…
READ MORE
blog
As a developer working on various Kotlin Multiplatform projects, whether for your job or…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu