Rust-like error handling in Kotlin using `?`, is it possible?

Viewed 181

The code below has several possible failures. For example, width could be null, or r could be false. In all cases, I should return a result.error() or something like that.

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else if (call.method=="registerTexture") {
      val entry: TextureRegistry.SurfaceTextureEntry = texture_registry.createSurfaceTexture();
      val surfaceTexture = entry.surfaceTexture();
      //TODO: return non-sucess when no width and height passed
      val width: Int = call.argument("width")!!
      val height: Int = call.argument("height")!!
      surfaceTexture.setDefaultBufferSize(width, height)
      val response = HashMap<String, Long>()
      RendererPlugin.surfaceTextureMap.put(entry, surfaceTexture)
      val r = RendererPlugin.registerSurfaceTextureNativeHandler(entry.id(), surfaceTexture)
      if (!r) {
        Log.d(LOG_TAG, "attention: failed result from registerSurfaceTextureNativeHandler")
      }
      response.put("textureId", entry.id())
      result.success(response)
    }
  }

On Rust, I'd make all of this into a closure which results Result<(), Error> and then execute the closure inside onMethodCall and if I got an error, I'd return an error. Also, the closure would be full of calls ending with ? so it automatically returns the error that has a From<> implementation that converts to Error.

How can I do this efficiently in Kotlin? Is there a way to make a closure and also easily return success or error in this closure, and then based on this result I call result.sucess or result.error?

2 Answers

There's a runCatching function you can call on anything. Within the following lambda closure, the object you called it on is "this". You can either return something for success, or throw something for failure. Then you can unpack this result into your Result object. error(), require() and check() are useful ways to throw under a certain condition without interrupting the flow of your code.

To return something from a lambda enclosure, you can either manually do it with return@nameOfFunctionLambdaIsPassedTo, or let the last expression of the lambda be the return value. A when expression can be used as the last (only) expression of your lambda, so the last thing in each branch of the when is the successful returned result (or a thrown error).

Here's an example:

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    call.runCatching {
        when(method) {
            "getPlatformVersion" -> "Android ${android.os.Build.VERSION.RELEASE}"
            "registerTexture" -> {
                 val entry = texture_registry.createSurfaceTexture()
                 val surfaceTexture = entry.surfaceTexture()
                 val width: Int = argument("width") ?: error("no width passed")
                 val height: Int = argument("height") ?: error("no height passed")
                 surfaceTexture.setDefaultBufferSize(width, height)
                 RendererPlugin.surfaceTextureMap.put(entry, surfaceTexture)
                 check(RendererPlugin.registerSurfaceTextureNativeHandler(entry.id(), surfaceTexture)) {
                     "attention: failed result from registerSurfaceTextureNativeHandler"
                 }
                 mapOf("textureId", entry.id())
            }
            else -> error("unsupported method name: $method")
        }
    }
        .onSuccess(result::success)
        .onFailure(result::failure)
}

If you want to avoid having to call onSuccess/onFailure, you can create a generic extension function you could use repeatedly like this:

inline fun MethodCall.runForResult(result: Result, block: MethodCall.() -> Any) {
    runCatching(block).onSuccess(result::success).onFailure(result::failure)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    call.runForResult(result) {
        when(method) {
            "getPlatformVersion" -> "Android ${android.os.Build.VERSION.RELEASE}"
            "registerTexture" -> {
                 val entry = texture_registry.createSurfaceTexture()
                 val surfaceTexture = entry.surfaceTexture()
                 val width: Int = argument("width") ?: error("no width passed")
                 val height: Int = argument("height") ?: error("no height passed")
                 surfaceTexture.setDefaultBufferSize(width, height)
                 RendererPlugin.surfaceTextureMap.put(entry, surfaceTexture)
                 check(RendererPlugin.registerSurfaceTextureNativeHandler(entry.id(), surfaceTexture)) {
                     "attention: failed result from registerSurfaceTextureNativeHandler"
                 }
                 mapOf("textureId", entry.id())
            }
            else -> error("unsupported method name: $method")
        }
    }
}

I hope this helps you.

Pay attention: I assume that result.success() accepts Any because you return String or HashMap as a parameter, but kotlin.Result doesn't have several generic success parameters. You can play with kotlin.Pair<A, B> as the success type of kotlin.Result :D

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    val finalResult: kotlin.Result<Any> = call.run {
        if (method == "getPlatformVersion") kotlin.Result.success("Android ${android.os.Build.VERSION.RELEASE}")
        else if (method == "registerTexture") {
            val entry: TextureRegistry.SurfaceTextureEntry = texture_registry.createSurfaceTexture();
            val surfaceTexture = entry.surfaceTexture();
            val width = argument("width") ?: return@run kotlin.Result.failure(IllegalArgumentException("width is null"))
            val height = argument("height") ?: return@run kotlin.Result.failure(IllegalArgumentException("height is null"))

            surfaceTexture.setDefaultBufferSize(width, height)
            val response = HashMap<String, Long>()
            RendererPlugin.surfaceTextureMap.put(entry, surfaceTexture)
            val r = RendererPlugin.registerSurfaceTextureNativeHandler(entry.id(), surfaceTexture)
            if (!r) return@run kotlin.Result.failure(IllegalArgumentException("r is false"))
            response.put("textureId", entry.id())
            kotlin.Result.success(response)
        }
        else kotlin.Result.failure(IllegalStateException("undefined call.method"))
    }

    with (finalResult) {
        onSuccess(result::success)
        onFailure(result::fail)
    }
}
Related