railway_dio

pub package pub points CI codecov style: lints License: MIT

A Dio client factory and a Response -> Either<NetworkFailure, T> mapper for railway-oriented error handling in Flutter/Dart.

How it works

  1. Build a Dio instance with buildDioClient(baseUrl: ...) — or use your existing Dio client.
  2. Call .toEither() on any Dio request future. You get back Either<NetworkFailure, T>Right(data) on success, or Left(failure) on failure.
  3. The failure type is assigned automatically based on what went wrong (timeout, no connection, 401 unauthorized, 4xx client error, 5xx server error, or unknown).

Why railway_dio over plain Dio?

Without railway_dio, every repository end up hand-rolling its own try-catch blocks and DioException handling:

try {
  final response = await dio.get('/something');
  return User.fromJson(response.data);
} on DioException catch (e) {
  if (e.type == DioExceptionType.connectionTimeout) {
    // handle timeout
  } else if (e.response?.statusCode == 401) {
    // handle unauthorized
  }
  // ...repeated across every single repository
}

toEither centralizes that branching once:

final result = await dio.get('/something').toEither((data) => User.fromJson(data));

result.match(
  (failure) => /* handle NetworkFailure */,
  (user) => /* use user */,
);

What is "Railway Oriented Programming"?

Railway Oriented Programming (ROP) is a functional pattern for modeling a chain of operations that can each succeed or fail, without littering your code with try/catch or null checks at every step. Picture two parallel tracks: a "success" track and a "failure" track. Each step runs on the success track, and the moment one step fails, execution switches to the failure track and every later step is skipped — the failure rides straight through to the end untouched.

In Dart, Either<L, R> (from fpdart) is the type that represents this: Left for failure, Right for success. railway_dio builds that pattern on top of dio, so every network call resolves into a single, well-typed Either<NetworkFailure, T> — you handle failure once, at the end of the chain, instead of after every call.

This package owns no business logic and no DTOs. It's feature-agnostic, so any number of features in an app can depend on it without depending on each other through it.

Installation

Add railway_dio to your pubspec.yaml:

dependencies:
  railway_dio: ^0.1.0
  fpdart: ^1.1.0

Usage

import 'package:railway_dio/railway_dio.dart';

final dio = buildDioClient(
  baseUrl: 'https://api.example.com',
);

final result = await dio
    .get<Map<String, dynamic>>('/users/1')
    .toEither((data) => User.fromJson(data));

result.match(
  (failure) => print('Failed: $failure'),
  (user) => print('User: ${user.name}'),
);

Exports

Export Purpose
buildDioClient Configures a standard Dio instance (base url, timeouts, default headers, interceptors).
NetworkFailure Sealed, freezed union of transport-level network failures.
toEither Extension on Future<Response<T>> — converts a Dio request into Either<NetworkFailure, R>.

NetworkFailure variants

Variant When
NetworkFailure.network(message) DNS failure, connection refused, offline device, or invalid SSL certificate.
NetworkFailure.timeout() Connection, send, or receive timeout exceeded.
NetworkFailure.unauthorized(errorBody) Response status was 401.
NetworkFailure.clientError(statusCode, message, errorBody) Response status was 4xx (excluding 401).
NetworkFailure.server(statusCode, message, errorBody) Response status was 5xx.
NetworkFailure.unknown(error) Any other unexpected exception or request cancellation.

Contributing

Generate freezed files:

dart pub get
dart run build_runner build --delete-conflicting-outputs

Libraries

railway_dio