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
Resultaliases coexist inrsbinder. AIDL methods usersbinder::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 plainrsbinder::Result<T>= Result<T, StatusCode>. BecauseStatusCodeimplementsFrom<StatusCode> for Status, propagating with?from aResult<_, StatusCode>into an AIDL method body (which returnsResult<_, 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:
| Variant | Meaning |
|---|---|
Ok | Operation completed successfully |
Unknown | An unspecified error occurred |
BadValue | Invalid parameter value |
UnknownTransaction | The transaction code is not recognized |
PermissionDenied | Caller does not have permission |
DeadObject | The remote process has died |
FailedTransaction | The transaction could not be completed |
NoMemory | Out of memory |
BadType | Wrong data type encountered |
NotEnoughData | The 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:
| Variant | Wire value | Meaning |
|---|---|---|
None | 0 | No error |
Security | -1 | Security / permission violation |
BadParcelable | -2 | Malformed parcelable data |
IllegalArgument | -3 | Invalid argument provided |
NullPointer | -4 | Unexpected null value |
IllegalState | -5 | Operation invalid for current state |
NetworkMainThread | -6 | Network operation on main thread (Java compatibility) |
UnsupportedOperation | -7 | Requested operation is not supported |
ServiceSpecific | -8 | Application-defined error with custom code |
Parcelable | -9 | Embedded user parcelable exception (Java compatibility) |
HasNotedAppOpsReplyHeader | -127 | Internal: reply parcel carries an AppOps noted-ops header |
HasReplyHeader | -128 | Internal: reply parcel carries a header (Java-specific marker) |
TransactionFailed | -129 | Low-level transaction failure |
JustError | -256 | Generic 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_erroris for what the caller's own logic cares about — "item not found", "quota exceeded" — with the codes declared as constants both sides share. A rawStatusCodelikePermissionDeniedorBadValueis for the IPC mechanism failing. -
Check
exception_code()before anything else.service_specific_error()andtransaction_error()are only meaningful for their own exception code; called on any other, they return zero rather than an error. -
Expect
DeadObjectin a long-running client. A service can restart.link_to_deathtells you when, so you can reconnect instead of failing every subsequent call — or letReconnectingdo it. -
StatusimplementsDisplayandstd::error::Error, so it works with?in a function returningBox<dyn Error>and prints usefully in a log.