Skip to content

Error handling

Every method returns app::Result<T>, which is Result<T, AppError> (crates/sdk/src/error.rs). An AppError is a code plus fields, the shape a client receives: { code, message }, with message the fields as a JSON object ({} when there are none).

A call that fails writes nothing: the node applies a call’s changes only when it ends Ok, and a failed call emits no events.

Declare them once, in the #[app::logic] module:

#[app::error]
pub enum Error {
NotFound,
Taken { start: u64 },
Invalid(String),
}

The variant name is the code; named fields keep their camelCase names, tuple fields are "0", "1"; u64, i64 and bytes are strings. Taken { start: 7 } reaches the client as code Taken, message {"start":"7"}. The ABI lists every code and its fields.

Return one with app::bail! or ?:

if title.is_empty() {
app::bail!(Error::Invalid("empty title".into()));
}
self.bookings.get(&id)?.ok_or_else(|| Error::NotFound.into())

For an ad-hoc error, AppError::new("Code") or AppError::with_fields("Code", json!({ .. })).

Source Code
guards::require_member / require_admin LoginRequired (not an account), Forbidden (below the role)
A Table write Duplicate with the unique constraint’s name, or "key"
Storage (encoding, limits, cursors) the StorageError variant, e.g. KeyTooLong, OutOfBounds, BadCursor
files and std::io Files, or Io for other io errors
Cross-app calls Xcall, XcallFailed (carrying the target’s error)
Outbound HTTP Fetch

StorageError, TableError and std::io::Error convert into AppError, so ? works on every SDK call.

Before your method runs, the generated handle refuses an unknown method (UnknownMethod), malformed arguments (BadArgs), a call run as a view (NotAView), and a caller the method does not allow (Forbidden, LoginRequired).

A panic, running out of fuel or memory, or any other trap ends the run without an app error; the call fails and writes nothing. Use errors for anything a client should handle, and keep panics for bugs.