Skip to main content

Catching Java exceptions in Swift via j2objc


TL;DR: it’s possible to handle Java-originating exceptions in Swift for j2objc-based projects. Scroll to the end for example code.

It’s getting more common to call j2objc-generated Objective-C code from Swift as iOS development shifts to this modern language. At a high level, we can imagine this means calling Java code from Swift. But Objective-C is an important link in this chain and it shapes the way Swift interacts with the code that started its life as Java.

j2objc does a great job of supporting Swift’s features when called with the --swift-friendly flag. This feature is particularly useful when the Java code is annotated with @Nonnull, @Nullable, and @ParametersAreNonnullByDefault to enforce Swift’s optionality at compile time and as you type in Xcode.

But there’s one important Java feature that gets lost in translation on the way to Swift: exceptions.

You might expect to catch Java exceptions from Swift like:

do {
    // original Java code throws IllegalArgumentException
    try javaObject.frobnicate(withFoo: bar)
} catch is JavaLangIllegalArgumentException {
    // handle exception
}

But when you try this, you’ll notice that Xcode complains that frobnicate() doesn’t throw such an exception. And if you move frobnicate() out of the do block, you’ll notice Xcode gives no warning that it can throw an exception. But your app will happily crash when that exception is thrown!

The reason for this mismatch is largely conventional. Objective-C exceptions are typically thrown for unrecoverable errors and, thus, by design, Swift cannot catch these exceptions. All exceptions are expected to be handled within Objective-C:
there’s no safe way to recover from Objective-C exceptions in Swift. To handle Objective-C exceptions, write Objective-C code that catches exceptions before they reach any Swift code.
So, although j2objc exposes Java exceptions in the generated Objective-C code, they aren’t further exposed to Swift.

However, Objective-C does have a class and a convention that is used similarly to Java exceptions though the syntax is a bit different. In short, functions that return a BOOL and accept an NSError** as their final argument return NO and set the NSError** can be handled similarly to Java methods that throw an exception.


With a little utility code, we can automatically translate JavaLangThrowable to NSError** and thus expose our Java Throwables (including Exceptions and Errors) to Swift.

I copied the technique from this Stack Overflow post and simply adjusted it so it copes with JavaLangThrowable instead of NSException. To save any confusion, I’m inlining the resulting code below.

First, create the following files in your project:

ObjC.h

@interface ObjC : NSObject

+ (BOOL)catchThrowable:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

ObjC.m

#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchThrowable:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
    @try {
        tryBlock();
        return YES;
    }


    @catch (JavaLangThrowable *throwable) {
        *error = [[NSError alloc] initWithDomain:throwable.name code:0 userInfo:throwable.userInfo];

        return NO;
    }
}

@end


And include the header file in your *-Bridging-Header.h:

#import "ObjC.h"

Now, you can handle exceptions in your Swift code like this:

do {
    try ObjC.catchThrowable {
        RecipeController().thrower()
    }
} catch {
    let desc = error.localizedDescription
    if desc.contains("IllegalArgumentException") {
        print("handle IllegalArgumentException here")
    } else if desc.contains("ParseException") {
        print("handle ParseException here")
    } else {
        print("handle all other exceptions here")
    }
}

Comments

Popular posts from this blog

Mapping tests from TestNG to JUnit

In the summer of 2020, J2ObjC's JRE emulation library (a fork of Android's libcore library ) was updated from Android Nougat to Android 10. The update consisted of adding new APIs, and updating the existing code. Apart from general functionality changes, one of the main goals of the update was to port new test cases to better verify that the JRE emulation library works correctly. While updating the java.time package, I came across a tck.java.time package containing approximately 15,000 new test cases written in TestNG. The problem? J2ObjC only supports JUnit. Instead of supporting both TestNG and JUnit, we decided to build testng2junit , a tool that converts TestNG tests to JUnit. How Can These Frame works be Mapped? To convert from TestNG to JUnit, we needed to determine where the two testing frameworks differed. Attempting to run the TestNG tests through the compiler gave us plenty of compile-time errors to sift through. These syntax errors, coupled with documentation from ...