Use this page with an LLM

Markdown

Errors

C examples use the package name demo and include demo.h. See C memory management for borrowed inputs and returned owners.

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 those targets. C returns a struct with an ok flag and a union holding the value or error; callers check the flag and free the result.

Supported Error Types

In C, read data.value when ok is true and data.error when it is false. Result<(), E> has no success value to read. The result owns whichever union member is active, including nested allocations or a returned class handle. Call the result’s generated cleanup function once; do not free the active member separately and then free the result.

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
  • C: the typed result contains the error record, enum, or string in data.error

String Errors

String errors use generic wrapper types:

  • Swift: FfiError
  • Kotlin: FfiException
  • Java: RuntimeException
  • C#: BoltException
  • TypeScript: FfiException
  • C: DemoString in the active result union for a package named demo

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 managed targets expose it through their error or exception types. C puts the error record in the result’s data.error field.

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 managed targets expose a sealed type hierarchy or tagged union. C uses the enum’s tag and data union inside the result’s data.error field.

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

C does not generate async functions yet. This section applies to the other targets.

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)
}