railway_dio
A Dio client factory and a Response -> Either<NetworkFailure, T> mapper for railway-oriented error handling in Flutter/Dart.
How it works
- Build a
Dioinstance withbuildDioClient(baseUrl: ...)— or use your existing Dio client. - Call
.toEither()on any Dio request future. You get backEither<NetworkFailure, T>—Right(data)on success, orLeft(failure)on failure. - 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
1. Without Decoder (Returns raw data)
If you don't pass a decoder, .toEither() returns Either<NetworkFailure, T> with the raw response data (e.g. Map<String, dynamic> or List):
import 'package:railway_dio/railway_dio.dart';
final dio = buildDioClient(baseUrl: 'https://api.example.com');
// Returns Either<NetworkFailure, Map<String, dynamic>>
final result = await dio.get<Map<String, dynamic>>('/users/1').toEither();
result.match(
(failure) => print('Failed: $failure'),
(json) => print('User Name: ${json['name']}'),
);
2. With Inline Decoder (Returns typed model)
Pass a decoder function as the first argument to convert the raw response directly into your domain model (e.g., User):
// Returns Either<NetworkFailure, User>
final result = await dio.get<Map<String, dynamic>>('/users/1').toEither(
(data) => User.fromJson(data),
);
result.match(
(failure) => print('Failed: $failure'),
(user) => print('Loaded User: ${user.name}'),
);
3. With Custom Error Decoder (Parses backend error JSON)
Pass an errorDecoder as the second argument to parse structured error payloads from your backend on 4xx/5xx responses:
final result = await dio.get<Map<String, dynamic>>('/users/1').toEither(
(data) => User.fromJson(data),
(errorJson) => ApiError.fromJson(errorJson),
);
result.match(
(failure) {
if (failure is NetworkClientFailure && failure.errorBody is ApiError) {
final apiError = failure.errorBody as ApiError;
print('API Error Code: ${apiError.code}');
}
},
(user) => print('Loaded 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. |
Related Packages
Part of the Railway Suite for functional error handling in Dart & Flutter:
railway_chopper— Railway-oriented error handling forpackage:chopper.
Contributing
Generate freezed files:
dart pub get
dart run build_runner build --delete-conflicting-outputs