# AppFlowy Error Handling: How flowy-error Bridges Rust and Flutter

> Discover how AppFlowy handles errors across Rust and Flutter using flowy-error. Learn about the unified error model, ErrorCode enum, and GlobalErrorCodeNotifier for seamless communication.

- Repository: [AppFlowy-IO/AppFlowy](https://github.com/AppFlowy-IO/AppFlowy)
- Tags: internals
- Published: 2026-03-03

---

**AppFlowy implements a unified cross-language error model through the `flowy-error` crate, where Rust defines an exhaustive `ErrorCode` enum and `FlowyError` struct that serialize to protobuf and broadcast to Flutter via the `GlobalErrorCodeNotifier` singleton.**

The AppFlowy codebase (AppFlowy-IO/AppFlowy) employs a type-safe error handling architecture that maintains consistency across the Rust backend and Flutter frontend. At the core of this system lies the `flowy-error` library, which provides a shared vocabulary of error conditions ensuring that backend failures translate into predictable frontend behavior.

## Architecture of the flowy-error System

### Rust Backend Layer

The Rust implementation defines errors in two primary components located in `frontend/rust-lib/flowy-error/src/`.

The `ErrorCode` enum in [`code.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/code.rs) establishes a comprehensive catalog of failure conditions:

```rust
// Located in frontend/rust-lib/flowy-error/src/code.rs
pub enum ErrorCode {
    Internal,
    UserUnauthorized,
    FileStorageLimitExceeded,
    AIResponseLimitExceeded,
    WorkspaceLimitExceeded,
    RecordNotFound,
    // ... additional variants
}

```

The `FlowyError` struct in [`errors.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/errors.rs) wraps an `ErrorCode` alongside a human-readable message and optional binary payload:

```rust
// Located in frontend/rust-lib/flowy-error/src/errors.rs
pub struct FlowyError {
    pub code: ErrorCode,
    pub msg: String,
    pub payload: Vec<u8>,
}

```

This struct provides helper methods such as `is_record_not_found()`, `is_ai_response_limit_exceeded()`, and `is_storage_limit_exceeded()`, plus `From` trait implementations for converting standard Rust error types.

### Flutter Frontend Layer

The Flutter side consumes errors through protobuf definitions in `frontend/appflowy_flutter/packages/appflowy_backend/protobuf/flowy-error/*.proto`. These generate `errors.pbserver.dart`, creating a Dart `FlowyError` class matching the Rust struct.

The `GlobalErrorCodeNotifier` in `frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart` functions as a singleton `ChangeNotifier`:

```dart
// Located in frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart
class GlobalErrorCodeNotifier extends ChangeNotifier {
  static FlowyError? _latestError;
  
  static void receiveErrorBytes(Uint8List bytes) {
    _latestError = FlowyError.fromBuffer(bytes);
    notifyListeners();
  }
}

```

The same file defines `FlowyErrorExtension`, adding convenience getters like `isAIResponseLimitExceeded` and `isStorageLimitExceeded` to the generated protobuf class.

## Error Flow from Rust to Flutter

The propagation of errors follows a strict five-stage pipeline:

1. **Backend Detection** – Rust functions return `FlowyResult<T>` (a type alias for `Result<T, FlowyError>`) when operations encounter failures.

2. **Protobuf Conversion** – The `FlowyError` struct serializes into a protobuf message using `encode_to_vec()`.

3. **FFI Transport** – The byte payload crosses the FFI boundary from Rust to Dart.

4. **Reception** – `GlobalErrorCodeNotifier.receiveError()` or `receiveErrorBytes()` deserializes the bytes and stores the error instance.

5. **UI Handling** – Widgets subscribe via `GlobalErrorCodeNotifier.add()` or inspect `FlowyResult` objects directly, using extension getters or explicit code matching to determine the appropriate response.

## Implementing AppFlowy Error Handling

### Creating Errors in the Rust Backend

Backend services instantiate errors using the `FlowyError` constructors:

```rust
use flowy_error::{ErrorCode, FlowyError};

fn verify_user_quota(current_bytes: u64, limit_bytes: u64) -> FlowyResult<()> {
    if current_bytes > limit_bytes {
        return Err(FlowyError::new(
            ErrorCode::FileStorageLimitExceeded,
            "You have reached your storage quota. Upgrade to continue."
        ));
    }
    Ok(())
}

```

### Broadcasting Errors Across the FFI Boundary

When the Rust backend encounters an error, it encodes and transmits the protobuf bytes:

```rust
match database_operation() {
    Ok(records) => dispatch_success(records),
    Err(flowy_error) => {
        let error_bytes = flowy_error.encode_to_vec();
        ffi_dispatch_error(error_bytes);
    }
}

```

On the Flutter side, the dispatch layer receives these bytes:

```dart
// Within the FFI receive handler
GlobalErrorCodeNotifier.receiveErrorBytes(errorBytes);

```

### Subscribing to Global Error Notifications

UI components register listeners through the singleton notifier:

```dart
final subscription = GlobalErrorCodeNotifier.add(
  onError: (FlowyError error) {
    if (error.isStorageLimitExceeded) {
      showStorageQuotaDialog();
    } else if (error.isAIResponseLimitExceeded) {
      showAILimitReachedToast();
    } else {
      showGenericErrorSnackBar(error.msg);
    }
  },
);

```

The notifier also supports `onErrorIf` callbacks to filter for specific error codes.

### Handling FlowyResult in Dart Services

Many AppFlowy services return `FlowyResult<T, FlowyError>` directly, allowing immediate error handling:

```dart
final result = await WorkspaceService.getWorkspace();

result.fold(
  (workspace) => _navigateToWorkspace(workspace),
  (error) {
    if (error.code == ErrorCode.WorkspaceLimitExceeded) {
      _showUpgradePlanPrompt();
    } else if (error.code == ErrorCode.UserUnauthorized) {
      _redirectToLogin();
    } else {
      _displayErrorMessage(error.msg);
    }
  },
);

```

## Key Source Files and Locations

| Path | Role |
|------|------|
| [`frontend/rust-lib/flowy-error/src/code.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-error/src/code.rs) | Defines the exhaustive `ErrorCode` enum containing all backend error conditions. |
| [`frontend/rust-lib/flowy-error/src/errors.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-error/src/errors.rs) | Implements the `FlowyError` struct with constructors, helper methods, and `From` trait implementations. |
| `frontend/appflowy_flutter/packages/appflowy_backend/protobuf/flowy-error/*.proto` | Protobuf schemas that generate Dart `FlowyError` classes via `errors.pbserver.dart`. |
| `frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart` | Contains `GlobalErrorCodeNotifier` and `FlowyErrorExtension` for reactive Flutter error handling. |
| `frontend/appflowy_flutter/lib/shared/error_page/error_page.dart` | Widget implementation for rendering full-screen error pages based on `FlowyError` objects. |

## Summary

- **Unified Vocabulary**: The `ErrorCode` enum in [`flowy-error/src/code.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/flowy-error/src/code.rs) serves as the single source of truth for error conditions across both Rust and Flutter environments.
- **Type-Safe Serialization**: Protobuf maintains type integrity when `FlowyError` crosses the FFI boundary between backend and frontend.
- **Reactive Broadcasting**: `GlobalErrorCodeNotifier` decouples error generation in Rust from error presentation in Flutter through a singleton observer pattern.
- **Ergonomic Inspection**: Helper methods like `is_record_not_found()` in Rust and extension getters like `isStorageLimitExceeded` in Dart simplify error condition checking.
- **Flexible Handling**: Developers can either subscribe to the global error stream for UI-wide alerts or handle `FlowyResult` objects locally within service methods.

## Frequently Asked Questions

### What is flowy-error in AppFlowy?

The `flowy-error` crate is AppFlowy's centralized error management library located in `frontend/rust-lib/flowy-error/`. It provides the foundational `ErrorCode` enum and `FlowyError` struct that standardize error representation across the entire application stack, ensuring that backend failures in Rust translate into actionable error objects in Flutter.

### How does AppFlowy communicate errors from Rust to Flutter?

Errors serialize to protobuf messages defined in `appflowy_backend/protobuf/flowy-error/`, cross the FFI boundary as `Uint8List` byte arrays, and deserialize into Dart objects. The `GlobalErrorCodeNotifier` singleton then broadcasts these errors to registered UI listeners, enabling reactive error handling throughout the Flutter widget tree.

### What ErrorCode values are available in flowy-error?

The `ErrorCode` enum in [`flowy-error/src/code.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/flowy-error/src/code.rs) includes variants such as `Internal`, `UserUnauthorized`, `FileStorageLimitExceeded`, `AIResponseLimitExceeded`, `WorkspaceLimitExceeded`, and `RecordNotFound`. New error conditions are added to this Rust enum and automatically propagate to Flutter through the protobuf code generation pipeline.

### How can Flutter widgets react to specific error conditions?

Widgets subscribe to `GlobalErrorCodeNotifier.add()` and inspect boolean flags via `FlowyErrorExtension` getters such as `isStorageLimitExceeded` and `isAIResponseLimitExceeded`. Alternatively, when processing `FlowyResult` objects returned from service calls, developers can perform exact matching against `error.code == ErrorCode.SpecificVariant` to trigger conditional UI logic.