Error Handling

Binder IPC introduces failure modes that do not exist in ordinary function calls: the remote process may crash, the kernel driver may reject a transaction, or the service may intentionally signal an application-level error. rsbinder represents all of these cases through two complementary types -- StatusCode for transport-level errors and Status for richer application-level errors that include exception codes and optional messages.

Core Types

rsbinder::BinderResult<T>

Every AIDL-generated method returns this type:

#![allow(unused)]
fn main() {
pub type BinderResult<T> = std::result::Result<T, Status>;
}

This is a standard Result whose error variant is a Status, and BinderResult is the name you will see in your generated files.

Note — two Result aliases coexist in rsbinder. AIDL methods use rsbinder::BinderResult<T> = Result<T, Status> (rich error with exception code + optional message), but several library-level APIs (Interface::dump, hub::default, hub::register_for_notifications, hub::get_service_debug_info) use the plain rsbinder::Result<T> = Result<T, StatusCode>. Because StatusCode implements From<StatusCode> for Status, propagating with ? from a Result<_, StatusCode> into an AIDL method body (which returns Result<_, Status>) works transparently.

StatusCode

StatusCode represents low-level transport errors that occur before or during a Binder transaction. It is named rsbinder::StatusCode.

Commonly encountered values:

VariantMeaning
OkOperation completed successfully
UnknownAn unspecified error occurred
BadValueInvalid parameter value
UnknownTransactionThe transaction code is not recognized
PermissionDeniedCaller does not have permission
DeadObjectThe remote process has died
FailedTransactionThe transaction could not be completed
NoMemoryOut of memory
BadTypeWrong data type encountered
NotEnoughDataThe parcel did not contain enough data

A StatusCode can be converted directly into a Status:

#![allow(unused)]
fn main() {
let status: rsbinder::Status = rsbinder::StatusCode::PermissionDenied.into();
}

Status

Status combines three pieces of information:

  • Exception code (ExceptionCode) -- categorizes the error (e.g. ServiceSpecific, Security, NullPointer).
  • Status code (StatusCode) -- provides transport-level detail.
  • Message (Option<String>) -- an optional human-readable description.

Key methods on Status:

#![allow(unused)]
fn main() {
// Check the category of the error
status.exception_code()      // -> ExceptionCode

// Get the transport error (only meaningful when exception is TransactionFailed)
status.transaction_error()   // -> StatusCode

// Get the service-specific error code (only meaningful when exception is ServiceSpecific)
status.service_specific_error() // -> i32

// The same code as your own type (see "Typed Error Codes" below)
status.service_error::<LookupError>() // -> Option<LookupError>

// The message the sender attached, if any
status.message()             // -> Option<&str>

// Check if the status represents success
status.is_ok()               // -> bool
}

ExceptionCode

ExceptionCode classifies the kind of error:

VariantWire valueMeaning
None0No error
Security-1Security / permission violation
BadParcelable-2Malformed parcelable data
IllegalArgument-3Invalid argument provided
NullPointer-4Unexpected null value
IllegalState-5Operation invalid for current state
NetworkMainThread-6Network operation on main thread (Java compatibility)
UnsupportedOperation-7Requested operation is not supported
ServiceSpecific-8Application-defined error with custom code
Parcelable-9Embedded user parcelable exception (Java compatibility)
HasNotedAppOpsReplyHeader-127Internal: reply parcel carries an AppOps noted-ops header
HasReplyHeader-128Internal: reply parcel carries a header (Java-specific marker)
TransactionFailed-129Low-level transaction failure
JustError-256Generic error fallback used internally by the library

Returning Errors from a Service

Service-Specific Errors

The most common way for a service to report an application-level error is through Status::new_service_specific_error. This sets the exception code to ServiceSpecific and carries an integer error code whose meaning is defined by the service:

#![allow(unused)]
fn main() {
fn ThrowServiceException(
    &self,
    code: i32,
) -> rsbinder::BinderResult<()> {
    Err(rsbinder::Status::new_service_specific_error(code, None))
}
}

You can also attach an optional message:

#![allow(unused)]
fn main() {
Err(rsbinder::Status::new_service_specific_error(
    -1,
    Some("resource not found".into()),
))
}

Typed Error Codes

An i32 is what crosses the wire, but it does not have to be what your code handles. A type that implements rsbinder::ServiceSpecificError carries the mapping once, so neither end writes a match over bare numbers:

#![allow(unused)]
fn main() {
// A plain Rust enum (needs the `macros` feature):
#[derive(rsbinder::ServiceSpecificError, Clone, Copy, PartialEq, Eq, Debug)]
#[repr(i32)]
pub enum LookupError {
    NotFound = 1,
    Busy = 2,
}

fn lookup(&self, key: &str) -> rsbinder::BinderResult<i32> {
    Err(rsbinder::Status::service_specific(
        LookupError::NotFound,
        Some("no such key"),
    ))
}
}

For an enum that comes from .aidl, the generated type is an open newtype rather than a Rust enum, so attach the trait with a macro instead:

#![allow(unused)]
fn main() {
rsbinder::impl_service_specific_error!(my_package::LookupError::LookupError);
}

Either way the bytes are the ones AOSP's Status::fromServiceSpecificError produces — a C++ or Java peer reads the same code it always did. The code space is i32, so an .aidl enum backed by long cannot be an error type; attaching the trait to one is a compile error rather than a value that truncates on the wire.

Unimplemented Methods

When a service does not support a particular transaction (for example, a method added in a newer version of the AIDL interface), return UnknownTransaction:

#![allow(unused)]
fn main() {
fn UnimplementedMethod(
    &self,
    _arg: i32,
) -> rsbinder::BinderResult<i32> {
    // Indicate that this method is not implemented
    Err(rsbinder::StatusCode::UnknownTransaction.into())
}
}

The .into() conversion automatically creates a Status with the TransactionFailed exception code.

Permission Errors

If your service enforces access control, return PermissionDenied:

#![allow(unused)]
fn main() {
fn restricted_operation(&self) -> rsbinder::BinderResult<()> {
    let caller = rsbinder::thread_state::CallingContext::default();
    if caller.uid != ALLOWED_UID {
        return Err(rsbinder::StatusCode::PermissionDenied.into());
    }
    Ok(())
}
}

Handling Errors on the Client Side

Checking for Service-Specific Errors

When calling a service method, always check the Result for errors. For service-specific errors, inspect the exception_code() first, then retrieve the integer code:

#![allow(unused)]
fn main() {
let result = service.ThrowServiceException(-1);
assert!(result.is_err());

let status = result.unwrap_err();
assert_eq!(
    status.exception_code(),
    rsbinder::ExceptionCode::ServiceSpecific,
);
assert_eq!(status.service_specific_error(), -1);
}

With a typed error the two checks collapse into one, because service_error::<T>() answers None unless the status really is a service-specific failure carrying one of T's values:

#![allow(unused)]
fn main() {
let status = service.lookup("k").unwrap_err();
match status.service_error::<LookupError>() {
    Some(LookupError::NotFound) => { /* create it */ }
    Some(LookupError::Busy) => { /* retry later */ }
    // Also reached for a code this build does not know: a peer compiled
    // against a newer contract sends one, and `service_specific_error()`
    // still has the raw number to log or forward.
    _ => log::warn!("unhandled: {status}"),
}
}

Distinguishing Error Categories

A practical error-handling pattern checks the exception code to determine how to react:

#![allow(unused)]
fn main() {
match service.some_method() {
    Ok(value) => {
        // Success
    }
    Err(status) => {
        match status.exception_code() {
            rsbinder::ExceptionCode::ServiceSpecific => {
                let code = status.service_specific_error();
                eprintln!("Service error {code}: {status}");
            }
            rsbinder::ExceptionCode::TransactionFailed => {
                let transport_err = status.transaction_error();
                eprintln!("Transport error: {transport_err:?}");
            }
            rsbinder::ExceptionCode::Security => {
                eprintln!("Permission denied: {status}");
            }
            other => {
                eprintln!("Unexpected error ({other}): {status}");
            }
        }
    }
}
}

Detecting a Dead Service

If the remote process crashes, methods will fail with DeadObject:

#![allow(unused)]
fn main() {
let result = service.some_method();
if let Err(ref status) = result {
    if status.transaction_error() == rsbinder::StatusCode::DeadObject {
        eprintln!("Service has died, attempting reconnection...");
    }
}
}

DeadObject alone does not prove the service died — a live handler of an in-process service, or of a service not built on rsbinder, can return it too (rsbinder sends its own remote handlers' DeadObject as FailedTransaction) — and over RPC the call that ended a session can return TimedOut instead. Reconnecting watches the service and reconnects for you.

Converting Between Error Types

rsbinder provides From implementations that make conversions between StatusCode, ExceptionCode, and Status straightforward:

#![allow(unused)]
fn main() {
// StatusCode -> Status
let status: rsbinder::Status = rsbinder::StatusCode::BadValue.into();

// ExceptionCode -> Status
let status: rsbinder::Status = rsbinder::ExceptionCode::IllegalArgument.into();

// Status -> StatusCode: the code the status carries, or `FailedTransaction`
// for an exception that carries none. A status built from
// `ExceptionCode::ServiceSpecific` carries `ServiceSpecific(0)`.
let code: rsbinder::StatusCode = status.into();
}

These conversions are used most often in the Err(...) return position of service methods, where .into() converts a StatusCode into the expected Status type automatically.

Tips

  • Split the two kinds of failure. Status::new_service_specific_error is for what the caller's own logic cares about — "item not found", "quota exceeded" — with the codes declared as constants both sides share. A raw StatusCode like PermissionDenied or BadValue is for the IPC mechanism failing.

  • Check exception_code() before anything else. service_specific_error() and transaction_error() are only meaningful for their own exception code; called on any other, they return zero rather than an error.

  • Expect DeadObject in a long-running client. A service can restart. link_to_death tells you when, so you can reconnect instead of failing every subsequent call — or let Reconnecting do it.

  • Status implements Display and std::error::Error, so it works with ? in a function returning Box<dyn Error> and prints usefully in a log.