data_channel 5.0.0 copy "data_channel: ^5.0.0" to clipboard
data_channel: ^5.0.0 copied to clipboard

data_channel (DC) is a simple dart utility for handling exceptions and data routing.

Introduction #

Data Channel for Dart and Flutter.

data_channel (DC) is a lightweight, type-safe utility for exception handling and null safety in Dart applications. Rather than cluttering your code with try-catch blocks and null checks, data_channel provides a clean, composable way to handle errors, data, and optional values.

An Opinionated Alternative to Result and Option Types:

data_channel is an opinionated alternative to traditional Result/Either and Option/Maybe patterns found in functional programming. Unlike standalone implementations, DC tightly couples error handling with optional data through its integrated Option type, providing maximum convenience and compile-time type safety.

Core Concepts:

  • DC (Data Channel) - Represents either an error or optional data (similar to Result/Either types)
  • Option - Represents presence (Some) or absence (None) of a value (eliminates nullable types)
  • Non-Nullable Guarantee - Some always contains non-null values; use None for absence
  • Tight Coupling - DC always wraps data in Option<Data>, enforcing explicit null handling
  • Type Safety - Explicit error and data types in your function signatures
  • Composability - Chain operations without repetitive error/null checking

Prerequisites #

Breaking Changes in 4.0.0:

  • Complete API redesign with Option type
  • Requires Dart 3.0+ for sealed class support

What version to use

  • Use data_channel 5.0.0 and above for Dart SDK >= 3.0.0
  • Use data_channel 3.0.0+2 for Dart SDK >= 2.12.0 and < 3.0.0

Installation #

Visit https://pub.dev/packages/data_channel#-installing-tab- for the latest version of data_channel


API Reference #

Data Channel (DC) API #

Constructors

  • DC.error(error) - Creates a DC containing an error
  • DC.auto(nullableData) - Automatically creates Some if non-null, None if null
  • DC.some(data) - Creates a DC with NON-NULL data wrapped in Some
  • DC.none() - Creates a DC with no data (None)
  • DC.fromOption(option) - Creates a DC from an existing Option without double-wrapping

Instance Methods

  • hasError - Returns true if this is a DCError
  • hasOptionalData - Returns true if this is DCData (with Some or None), false if DCError
    • Note: To check if actual data is present, use the Option's isSome/isNone methods
  • fold<U>({onError, onData}) - Pattern match on error or data, returning a value
  • mapError<NewErr>(transform) - Transform the error type while preserving data

Static Methods

  • DC.forwardErrorOr(dc, newData) - Forward error if present, otherwise create new DCData with provided data
  • DC.forwardErrorOrNull(dc) - Forward error if present, otherwise create DCData with None
  • DC.forwardErrorOrElse(dc, builder) - Forward error if present, otherwise lazily call builder to transform data

Option API #

The Option<T> type represents optional values and is used within DCData to hold the actual data.

Constructors

  • Some(value) - Creates an Option containing a NON-NULL value
  • None() - Creates an empty Option (use None<T>() for explicit typing)
  • Option.auto(nullable) - Automatically creates Some if non-null, None if null (requires explicit type for null: Option<T>.auto(null))

Instance Methods

  • isSome - Returns true if contains a value (guaranteed non-null)
  • isNone - Returns true if empty
  • tryMaybe() - Returns the value or null
  • orElse(defaultValue) - Returns the value or a default
  • orElseGet(getDefault) - Returns the value or computes a default (lazy)
  • map<U>(transform) - Transform the contained value
  • flatMap<U>(transform) - Transform and flatten nested Options
  • filter(predicate) - Returns None if predicate fails
  • fold<U>({onSome, onNone}) - Pattern match on Some or None

Type Aliases

  • DCOption<T> - Alias for Option<T>
  • DCSome<T> - Alias for Some<T>
  • DCNone<T> - Alias for None<T>

Examples #

Non-Nullable Guarantee #

With the extends Object constraint, Some ALWAYS contains non-null values:

void main() {
  final userOption = Some(User('1', 'Alice'));

  if (userOption.isSome) {
    // Safe to unwrap - guaranteed non-null!
    final user = userOption.tryMaybe()!;
    print('User: ${user.name}'); // No null check needed
  }

  // None represents absence, not null value
  final emptyOption = None<User>();
  print(emptyOption.isNone); // true
  print(emptyOption.tryMaybe()); // null (returns null, doesn't contain null)
}

⚠️ Warning: Type casting and dynamic can bypass this guarantee. Use DC.auto() or Option.auto() for nullable values instead of casting.

void main() {
  // Unsafe - can crash at runtime
  dynamic value = null;
  Some(value as User);

  // Safe - handles null correctly
  DC.auto(nullableUser);
  Option.auto(nullableUser);
}

DC.auto - Auto-handle Nullable Values #

Automatically wraps nullable values in the appropriate Option variant. Use this to eliminate manual null checks when working with nullable data:

Future<DC<Exception, User>> getUserById(String id) async {
  try {
    final User? user = await database.findUser(id); // might be null

    // Automatically creates Some(user) or None() based on null check
    return DC<Exception, User>.auto(user);

    // Instead of manual:
    // return user != null ? DC.some(user) : DC.none();
  } on Exception catch (e) {
    return DC.error(e);
  }
}

Creates a DC from an existing Option WITHOUT double-wrapping. Use this when you already have an Option from another operation and want to lift it into a DC:

Future<DC<Exception, Profile>> getProfileFromCache() async {
  try {
    // Cache returns Option<Profile>
    final Option<Profile> cachedProfile = cache.get('profile');

    // Lift Option directly into DC WITHOUT creating Option<Option<Profile>>
    return DC<Exception, Profile>.fromOption(cachedProfile);

    // Instead of manual:
    // return cachedProfile.fold(
    //   onSome: (p) => DC.some(p),
    //   onNone: () => DC.none(),
    // );
  } on Exception catch (e) {
    return DC.error(e);
  }
}

Basic Usage #

Return either error or data from any method:

import 'package:data_channel/data_channel.dart';

Future<DC<Exception, LoginModel>> getSomeLoginData() async {
  try {
    return DC<Exception, LoginModel>.some(someData);
  } on Exception catch (e) {
    return DC<Exception, LoginModel>.error(e);
  }
}

Check for errors in the calling code:

void doSomething() async {
  final value = await getSomeLoginData();

  if (value.hasError) {
    // handle error case
  } else if (value.hasOptionalData) {
    // handle success case - but data might be None!
    // Use fold or access .data to check Option
  }
}

Using DC.fold #

Handle both cases exhaustively with a clean functional approach:


final message = result.fold(
  onError: (error) => 'Operation failed: $error',
  onData: (dataOption) =>
      dataOption.fold(
        onSome: (data) => 'Success: $data',
        onNone: () => 'No data available',
      ),
);

// Use in widget building
final widget = result.fold(
  onError: (error) => ErrorWidget(error),
  onData: (dataOption) =>
      dataOption.fold(
        onSome: (data) => SuccessView(data),
        onNone: () => EmptyStateWidget(),
      ),
);

DC.forwardErrorOr - Propagate Errors #

Eliminate redundant error checks when transforming data models. DC.forwardErrorOr automatically propagates errors while allowing you to transform successful data:

Future<DC<Exception, UserModel>> checkSomethingAndReturn() async {
  final loginData = await getSomeLoginData();

  // If loginData has error, it's forwarded automatically
  // If loginData has success, old data is discarded and new UserModel is wrapped
  return DC.forwardErrorOr(
    loginData,
    UserModel(id: 'some-id'),
  );
}

DC.forwardErrorOrNull - Propagate Errors, Discard Data #

When you want to acknowledge success but don't need to return data:

Future<DC<Exception, void>> deleteUser(String id) async {
  final result = await apiDeleteUser(id);

  // Forward error if present, otherwise return success with no data
  return DC.forwardErrorOrNull(result);
}

DC.forwardErrorOrElse - Transform with Option Methods #

The most powerful forwarding method - use Option transformations on the data:

Future<DC<Exception, Profile>> createProfile() async {
  final userResult = await fetchUser();

  // Return Some directly
  return DC.forwardErrorOrElse(
    userResult,
        (_) => Some(Profile.defaultProfile()),
  );
}

// Transform the data
Future<DC<Exception, String>> getUserName() async {
  final userResult = await fetchUser();

  return DC.forwardErrorOrElse(
    userResult,
        (userData) => userData.map((user) => user.name),
  );
}

// Filter with validation
Future<DC<Exception, User>> getVerifiedUser() async {
  final userResult = await fetchUser();

  return DC.forwardErrorOrElse(
    userResult,
        (userData) => userData.filter((user) => user.isVerified),
  );
}

// Provide fallback
Future<DC<Exception, String>> getDisplayName() async {
  final userResult = await fetchUser();

  return DC.forwardErrorOrElse(
    userResult,
        (userData) => Some(userData.map((user) => user.name).orElse('Guest')),
  );
}

DC.mapError - Transform Errors #

Transform error types while preserving data:

// Convert technical errors to user-friendly messages
final userFriendly = apiResult.mapError(
      (error) => UserFacingException(error.message),
);

// Chain error transformations
final logged = apiResult
    .mapError((e) => logError(e))
    .mapError((e) => UserFacingException(e.message));

Working with Option Inside DCData #

When you have DCData, you can work with the inner Option:

void main() {
  final result = await getUserData();

  result.fold(
    onError: (e) => print('Error: $e'),
    onData: (userOption) {
      // userOption is Option<User>

      // Check if data exists
      if (userOption.isSome) {
        final user = userOption.tryMaybe()!; // Safe - guaranteed non-null
        print('User: ${user.name}');
      }

      // Or use Option methods
      final name = userOption
          .map((user) => user.name)
          .orElse('Anonymous');

      // Or use fold
      userOption.fold(
        onSome: (user) => print('Found: ${user.name}'),
        onNone: () => print('No user data'),
      );
    },
  );
}

Example: HTTP API with DC #

Return data or error from an API call without leaking try-catch everywhere:

import 'dart:convert';
import 'package:data_channel/data_channel.dart';
import 'package:http/http.dart' as http;

Future<DC<Exception, StarwarsResponse>> getStarwarsCharacters() async {
  try {
    final response = await http.get(
      Uri.parse('https://starwars-api.com/characters'),
    );

    if (response.statusCode != 200) {
      return DC.error(Exception('HTTP ${response.statusCode}'));
    }

    if (response.body.isEmpty) {
      return DC.none(); // Success but no data
    }

    final data = StarwarsResponse.fromJson(
      json.decode(response.body) as Map<String, dynamic>,
    );

    return DC.some(data);
  } on Exception catch (e) {
    return DC.error(e);
  }
}

Example: Consuming the API Result #

void loadCharacters() async {
  final result = await getStarwarsCharacters();

  final message = result.fold(
    onError: (error) => 'Failed to load: $error',
    onData: (characters) =>
        characters.fold(
          onSome: (data) => 'Loaded ${data.count} characters',
          onNone: () => 'No characters found',
        ),
  );

  print(message);
}

Example: Chaining Operations #

Build complex workflows by chaining DC operations:

void main() {
  Future<DC<Exception, ProfileView>> loadUserProfile(String userId) async {
    // Step 1: Fetch user
    final userResult = await fetchUser(userId);

    // Step 2: Validate user is verified, forward error if any
    final verifiedUserResult = DC.forwardErrorOrElse(
      userResult,
          (userData) => userData.filter((user) => user.isVerified),
    );

    // Step 3: Load profile data, forward error if any
    final profileResult = DC.forwardErrorOrElse(
      verifiedUserResult,
          (userData) => userData.map((user) => user.profileId),
    );

    // Step 4: Transform to view model
    return DC.forwardErrorOrElse(
      profileResult,
          (profileIdOption) =>
          profileIdOption.map(
                (profileId) => ProfileView(id: profileId, verified: true),
          ),
    );
  }

  // Use it
  final result = await loadUserProfile('123');

  result.fold(
    onError: (e) => showError('Could not load profile: $e'),
    onData: (profileView) =>
        profileView.fold(
          onSome: (view) => showProfile(view),
          onNone: () => showMessage('User not verified'),
        ),
  );
}

Example: Flutter Widget #

class UserProfileWidget extends StatelessWidget {
  final DC<Exception, User> userResult;

  const UserProfileWidget({required this.userResult});

  @override
  Widget build(BuildContext context) {
    return userResult.fold(
      onError: (error) =>
          ErrorCard(
            message: 'Failed to load user',
            error: error,
          ),
      onData: (userData) =>
          userData.fold(
            onSome: (user) =>
                UserCard(
                  name: user.name,
                  email: user.email,
                  verified: user.isVerified,
                ),
            onNone: () =>
            const EmptyStateCard(
              message: 'No user data available',
            ),
          ),
    );
  }
}

Example: Option Standalone Usage #

You can also use Option independently for nullable value handling:

import 'package:data_channel/data_channel.dart';

// Create Options
final some = Some(42);
final none = None<int>();
final fromNullable = Option<int>.auto(possiblyNullValue); // Explicit type needed

// Transform values
final doubled = some.map((x) => x * 2); // Some(84)
final noneDoubled = none.map((x) => x * 2); // None()

// Provide defaults
final value1 = some.orElse(0); // 42
final value2 = none.orElse(0); // 0

// Filter
final evenOnly = some.filter((x) => x.isEven); // Some(42)
final oddOnly = some.filter((x) => x.isOdd); // None()

// Chain operations
final result = Option.auto(user)
    .filter((u) => u.isActive)
    .map((u) => u.email)
    .map((e) => e.toLowerCase())
    .orElse('no-email@example.com');

// Pattern matching
final message = option.fold(
  onSome: (value) => 'Got: $value',
  onNone: () => 'Nothing here',
);

Buy me a coffee #

Help keep this package free and open for everyone.
PayPal: paypal.me/ganeshrvel

Contact #

Feel free to reach out at ganeshrvel@outlook.com

About #

License #

data_channel | Data Channel for Dart and Flutter
MIT License

Copyright © 2018-Present Ganesh Rathinavel

7
likes
160
points
149
downloads

Publisher

verified publisherganeshrvel.com

Weekly Downloads

data_channel (DC) is a simple dart utility for handling exceptions and data routing.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

meta

More

Packages that depend on data_channel