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

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 establishes a comprehensive catalog of failure conditions:

// 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 wraps an ErrorCode alongside a human-readable message and optional binary payload:

// 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:

// 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:

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:

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:

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

Subscribing to Global Error Notifications

UI components register listeners through the singleton notifier:

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:

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 Defines the exhaustive ErrorCode enum containing all backend error conditions.
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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →