Exceptions and Error Handling
This content is for v1. Switch to the latest version for up-to-date documentation.
The bridge’s wire layer catches all C++ exceptions and encodes them as error frames before handing them to Dart; exceptions thrown by Dart closures also return to C++ via the wire. This page explains the propagation rules for each path.
Core Principles
Section titled “Core Principles”Exceptions never cross the FFI boundary. Whether thrown from C++ or Dart, exceptions are caught by the bridge and encoded as wire error frames.
Unified handling on the C++ side:
try { // call the user function} catch (const std::exception& e) { post_error(req, e.what());} catch (...) { post_error(req, "unknown error");}C++ Exceptions → Dart
Section titled “C++ Exceptions → Dart”BRIDGE_SYNC
Section titled “BRIDGE_SYNC”BRIDGE_SYNCstd::int32_t divide(std::int32_t a, std::int32_t b) { if (b == 0) throw std::runtime_error("divide by zero"); return a / b;}Dart side:
try { final r = bridge.divide(a: 10, b: 0);} on StateError catch (e) { print(e.message); // "divide by zero"}BRIDGE_ASYNC
Section titled “BRIDGE_ASYNC”Exceptions thrown in coroutines (including those thrown by co_await) are caught by the wire dispatch.
BRIDGE_ASYNCasync_simple::coro::Lazy<std::string> fail() { throw std::runtime_error("async failed"); co_return "";}Dart side:
try { await bridge.fail();} on StateError catch (e) { print(e.message); // "async failed"}BRIDGE_NORMAL
Section titled “BRIDGE_NORMAL”Exceptions in the thread pool are first caught by async-simple’s awaiter, then re-thrown at the co_await on the io thread, where the wire dispatch catches them.
BRIDGE_NORMALstd::string normal_fail() { throw std::runtime_error("normal failed");}Dart side also receives StateError.
spawn_blocking
Section titled “spawn_blocking”async_simple::coro::Lazy<int> compute() { auto result = co_await dcb::spawn_blocking([] { throw std::runtime_error("blocking failed"); return 0; }); co_return result;}The exception is caught in the thread pool and re-thrown at the co_await. If you call this inside BRIDGE_ASYNC, the wire layer eventually catches it and passes it to Dart.
Dart Exceptions → C++
Section titled “Dart Exceptions → C++”When C++ calls a Dart closure (DartFn), exceptions thrown by the Dart closure are returned to C++.
Dart side
Section titled “Dart side”Future<String> greet(String name) async { if (name.isEmpty) throw Exception('name cannot be empty'); return 'Hello, $name!';}C++ side
Section titled “C++ side”BRIDGE_ASYNCasync_simple::coro::Lazy<std::string> call_greet( dcb::DartFn<std::string(std::string)> callback, std::string name) { try { auto reply = co_await callback(name); co_return reply; } catch (const std::runtime_error& e) { // e.what() contains "Exception: name cannot be empty" co_return std::string("error: ") + e.what(); }}Error Frame Format
Section titled “Error Frame Format”Payload of the responseErr frame:
code i32 error code (currently always 0)message string error messageThe Dart generated code converts this into a StateError.
Recommendations
Section titled “Recommendations”- Express business-level errors preferentially through return values or
std::optional, rather than relying on exceptions - Reserve exceptions for truly unrecoverable problems
- Do not swallow exceptions after catching
(...); at least log them