Skip to content

Not every crash can be caught

Abdul-Qudus Rufai 1 min read

This one is a concept I learned from a decision in the Nsila code, not from a crash I debugged myself.

Nsila uses MediaPipe for hand tracking, which can run on the phone’s CPU or GPU. The GPU sounds like the obvious choice for speed. The app defaults to the CPU, and this comment explains why:

// CPU (XNNPACK) by default: the GPU delegate can crash natively on some budget phone GPUs,
// and a native crash can't be caught.

The important part is the second line. Coming from JavaScript, I am used to the idea that an error can be caught somewhere.

Two kinds of failure

Kotlin code runs on a managed runtime. When something goes wrong there, an exception is thrown, and try/catch can handle it.

MediaPipe’s engine is native code that runs underneath that runtime. If native code crashes, for example inside a graphics driver, the operating system ends the whole process. No exception is thrown, your catch block never runs, and the user is simply back on the home screen.

What the code can and cannot protect

Failing to set up the GPU path does arrive as a normal exception, so the code can catch it and fall back to the CPU:

try {
    HandLandmarker.createFromOptions(context, options)
} catch (e: Exception) {
    if (delegate == Delegate.GPU) createLandmarker(context, Delegate.CPU) else throw e
}

That fallback cannot help with a native crash later, while frames are being processed. The only protection against that is to avoid the risky path, which is why the default matters more than the fallback.

For an app aimed at the budget phones common in Nigeria, a slower path that keeps running is a better trade than a faster one that may take the whole app down.

Read the Nsila case study