Errors

Rust represents fallible operations with Result<T, E>. Target languages have different conventions: Swift uses throws, Kotlin, Java, and C# use exceptions, and TypeScript uses try/catch. BoltFFI converts Result return types into the native error handling mechanism of each platform. When a function returns Err, it becomes a thrown error or exception in the target language.

Supported Error Types

The error type in Result<T, E> can be:

  • String or &'static str - becomes a generic error with a message
  • A struct marked with #[error] - becomes a structured error type
  • An enum marked with #[error] - becomes an error enum

The #[error] attribute marks types as error types. Generated bindings expose them through each target’s native error model:

  • Swift: error types conform to Error
  • Kotlin: error types extend Exception
  • Java: error enums and classes include a nested Exception type that extends RuntimeException
  • C#: generated methods throw BoltException for string errors or typed *Exception wrappers that expose the original error value

String Errors

String errors use generic wrapper types:

  • Swift: FfiError
  • Kotlin: FfiException
  • Java: RuntimeException
  • C#: BoltException
  • TypeScript: FfiException

Custom error types are thrown directly or wrapped as the native target requires.

The simplest approach is returning Result<T, String> or Result<T, &'static str>. The error message is captured in a generic error type.

RustSource
#[export]
pub fn parse_int(s: &str) -> Result<i32, String> {
  s.parse()
      .map_err(|e| format!("parse failed: {}", e))
}
public struct FfiError: Error {
  public let message: String
}

func parseInt(s: String) throws -> Int32

do {
  let n = try parseInt(s: "42")
} catch let error as FfiError {
  print(error.message)
}

Struct Errors

For structured error information, define a struct with #[error]. The struct becomes a throwable error type in both languages.

RustSource
#[error]
pub struct ParseError {
  pub line: u32,
  pub column: u32,
  pub message: String,
}

#[export]
pub fn parse_config(
  input: &str
) -> Result<Config, ParseError> {
  Err(ParseError {
      line: 10,
      column: 5,
      message: "unexpected token".into(),
  })
}
public struct ParseError: Error {
  public let line: UInt32
  public let column: UInt32
  public let message: String
}

func parseConfig(input: String) throws -> Config

do {
  let config = try parseConfig(input: src)
} catch let error as ParseError {
  print("\(error.line):\(error.column): \(error.message)")
}

Enum Errors

Error enums let you represent distinct failure cases. Simple enums (no associated data) become native enums. Enums with payloads become sealed types.

Simple Enums

RustSource
#[error]
pub enum AuthError {
  InvalidCredentials,
  SessionExpired,
  AccountLocked,
}

#[export]
pub fn login(
  user: &str, pass: &str
) -> Result<Session, AuthError> {
  Err(AuthError::InvalidCredentials)
}
public enum AuthError: Error {
  case invalidCredentials
  case sessionExpired
  case accountLocked
}

func login(user: String, pass: String) throws -> Session

do {
  let session = try login(user: u, pass: p)
} catch let error as AuthError {
  switch error {
  case .invalidCredentials:
      print("Wrong username or password")
  case .sessionExpired:
      print("Please log in again")
  case .accountLocked:
      print("Account is locked")
  }
}

Enums with Payloads

When enum variants carry associated data, the error becomes a sealed type hierarchy.

RustSource
#[error]
pub enum ApiError {
  Network { message: String },
  NotFound,
  RateLimited { retry_after: u32 },
}

#[export]
pub fn fetch_user(id: u64) -> Result<User, ApiError> {
  Err(ApiError::RateLimited { retry_after: 30 })
}
public enum ApiError: Error {
  case network(message: String)
  case notFound
  case rateLimited(retryAfter: UInt32)
}

func fetchUser(id: UInt64) throws -> User

do {
  let user = try fetchUser(id: 42)
} catch let error as ApiError {
  switch error {
  case .network(let message):
      print("Network error: \(message)")
  case .notFound:
      print("User not found")
  case .rateLimited(let seconds):
      print("Try again in \(seconds)s")
  }
}

Async Errors

Async functions that return Result work the same way. The error is delivered through the target language’s native error handling when the async operation completes.

RustSource
#[export]
pub async fn fetch_data(
  url: &str
) -> Result<Vec<u8>, FetchError> {
  let response = client.get(url).await?;
  Ok(response.bytes().await?)
}
func fetchData(url: String) async throws -> Data

do {
  let data = try await fetchData(url: endpoint)
  process(data)
} catch let error as FetchError {
  handle(error)
}