Either وأنواع الفشل ومعالجة الأخطاء الوظيفية
نمذج الأخطاء القابلة للاسترداد كقيم باستخدام dartz Either وأنواع الفشل المختومة
Either وأنواع الفشل ومعالجة الأخطاء الوظيفية درس مجاني في Flutter Mobile Development على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Flutter Mobile Development، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.
بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.
Errors as Values
In Clean Architecture, your domain and data layers should not crash the UI with raw exceptions. Instead of throwing across layer boundaries, you model recoverable errors as return values.
The dartz package gives you Either<L, R>: a value that is either a Left (by convention, the failure) or a Right (the success). The caller is forced to handle both.
Left= the error path (aFailure)Right= the happy path (your data)
This turns invisible throw/catch control flow into an explicit, type-checked contract.
// A repository method signature in the domain layer
// Future<Either<Failure, User>> getUser(String id);
//
// Left -> something went wrong (a Failure)
// Right -> the success value (a User)Modeling Either Ourselves
Before reaching for dartz, it helps to see what Either actually is. It is just a sealed type with two cases. Dart 3 sealed classes make the idea concrete and exhaustive.
The key property: a function returning Either always returns one of two shapes, and the compiler can verify you handled both in a switch.
sealed class Either<L, R> {}
class Left<L, R> extends Either<L, R> {
final L value;
Left(this.value);
}
class Right<L, R> extends Either<L, R> {
final R value;
Right(this.value);
}
Either<String, int> parsePositive(String s) {
final n = int.tryParse(s);
if (n == null) return Left('not a number');
if (n <= 0) return Left('must be positive');
return Right(n);
}
void main() {
final result = parsePositive('42');
final msg = switch (result) {
Left(value: final e) => 'Error: $e',
Right(value: final v) => 'Got $v',
};
print(msg);
}Sealed Failure Types
Rather than putting a raw String on the Left, Clean Architecture uses a sealed hierarchy of Failure types. Each subtype names a distinct, recoverable error your domain cares about.
ServerFailure— backend returned an errorCacheFailure— local storage failedNetworkFailure— device is offlineValidationFailure— input was invalid
Because the base is sealed, a switch over a Failure is checked for exhaustiveness — add a new failure and the compiler points you at every place that must handle it.
sealed class Failure {
final String message;
const Failure(this.message);
}
class ServerFailure extends Failure {
final int? statusCode;
const ServerFailure(super.message, {this.statusCode});
}
class CacheFailure extends Failure {
const CacheFailure(super.message);
}
class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
class ValidationFailure extends Failure {
const ValidationFailure(super.message);
}Returning Either from a Repository
Here is the classic Clean Architecture repository pattern. The data source throws exceptions; the repository catches them at the boundary and converts each into a typed Failure on the Left.
This is the only place try/catch lives. Above this line, the rest of your app deals exclusively in Either<Failure, T>.
import 'package:dartz/dartz.dart';
class UserRepository {
final RemoteDataSource remote;
UserRepository(this.remote);
Future<Either<Failure, User>> getUser(String id) async {
try {
final user = await remote.fetchUser(id);
return Right(user);
} on ServerException catch (e) {
return Left(ServerFailure(e.message, statusCode: e.code));
} on SocketException {
return const Left(NetworkFailure('No internet connection'));
}
}
}fold: Collapsing Either into One Value
The primary way to consume an Either is fold. It takes two functions — one for Left, one for Right — and runs whichever case applies, returning a single value of the same type.
fold forces you to handle the failure. You cannot accidentally read the success while ignoring the error, the way a forgotten try/catch lets you.
import 'package:dartz/dartz.dart';
String describe(Either<Failure, int> result) {
return result.fold(
(failure) => 'Failed: ${failure.message}',
(value) => 'Success: $value',
);
}
void main() {
final ok = const Right<Failure, int>(7);
final err = const Left<Failure, int>(CacheFailure('disk full'));
print(describe(ok)); // Success: 7
print(describe(err)); // Failed: disk full
}fold Returns a Value, Not Side Effects
A common confusion: fold is for producing a value, not for branching side effects. In Flutter you typically fold an Either into a UI state object.
Notice both branches return the same type (here, a ViewState). If the branches returned different types, the result would be Object and you would lose type safety — a frequent beginner mistake.
sealed class ViewState {}
class ErrorState extends ViewState {
final String message;
ErrorState(this.message);
}
class LoadedState extends ViewState {
final int data;
LoadedState(this.data);
}
// In a Bloc/Cubit/Notifier:
// final ViewState next = result.fold(
// (f) => ErrorState(f.message),
// (v) => LoadedState(v),
// );Transforming the Right with map
Either is right-biased: map transforms the success value and leaves a Left untouched. This lets a use case reshape data without unwrapping and rewrapping.
If the value is a Left failure, map is a no-op — the failure flows straight through. This is how you build pipelines that short-circuit on the first error.
import 'package:dartz/dartz.dart';
void main() {
Either<String, int> success = const Right(10);
Either<String, int> failure = const Left('boom');
// map only touches the Right side
print(success.map((v) => v * 2)); // Right(20)
print(failure.map((v) => v * 2)); // Left(boom) — unchanged
}Chaining with flatMap (bind)
When each step itself returns an Either, plain map would nest you into Either<F, Either<F, T>>. Use flatMap (also spelled bind in dartz) to chain failure-producing steps so the first Left short-circuits the rest.
This is the functional replacement for a chain of try/catch blocks: validation, then parsing, then a lookup — any failure stops the pipeline and surfaces as the final Left.
sealed class Either<L, R> {
Either<L, T> flatMap<T>(Either<L, T> Function(R) f) =>
switch (this) {
Left(value: final l) => Left(l),
Right(value: final r) => f(r),
};
}
class Left<L, R> extends Either<L, R> {
final L value;
Left(this.value);
}
class Right<L, R> extends Either<L, R> {
final R value;
Right(this.value);
}
Either<String, int> parse(String s) =>
int.tryParse(s) is int n ? Right(n) : Left('bad int');
Either<String, int> mustBePositive(int n) =>
n > 0 ? Right(n) : Left('not positive');
void main() {
final r = parse('5').flatMap(mustBePositive);
print(switch (r) {
Left(value: final e) => 'fail: $e',
Right(value: final v) => 'ok: $v',
});
}Use Cases Return Either Too
In Clean Architecture, a use case sits between the presentation layer and the repository. It also returns Either<Failure, T>, often adding domain-level validation that produces its own ValidationFailure.
The use case never knows or cares whether the failure came from the network or from its own checks — both are just Failure values flowing on the Left.
import 'package:dartz/dartz.dart';
class GetUser {
final UserRepository repo;
GetUser(this.repo);
Future<Either<Failure, User>> call(String id) async {
if (id.trim().isEmpty) {
return const Left(ValidationFailure('id must not be empty'));
}
return repo.getUser(id);
}
}Consuming Either in the UI Layer
At the edge — inside a Bloc, Cubit, or Riverpod notifier — you finally fold the Either into emitted UI states. The widget tree then reacts to those states.
Because every failure is a typed subtype of the sealed Failure, you can switch on it to show tailored messages: a retry button for NetworkFailure, a form error for ValidationFailure, and so on.
Future<void> loadUser(String id) async {
emit(UserLoading());
final result = await getUser(id);
result.fold(
(failure) => emit(UserError(_mapFailure(failure))),
(user) => emit(UserLoaded(user)),
);
}
String _mapFailure(Failure f) => switch (f) {
NetworkFailure() => 'You are offline. Tap to retry.',
ServerFailure(:final statusCode) => 'Server error ($statusCode).',
CacheFailure() => 'Could not read local data.',
ValidationFailure(:final message) => message,
};Either vs Throwing Exceptions
Why prefer Either over exceptions across layers?
- Explicit contracts — the return type tells callers a failure is possible; exceptions are invisible in the signature.
- Exhaustiveness — sealed
Failure+switchmeans a new error variant is a compile-time prompt, not a runtime surprise. - Composability —
map/flatMapchain steps and short-circuit cleanly.
Reserve real throw for unrecoverable, programmer-error situations (e.g. assert, invariant violations). Use Either<Failure, T> for expected, recoverable outcomes like network or validation errors.
Quick Check
Test your understanding of functional error handling in Clean Architecture.
Recap
You learned to model recoverable errors as values:
Either<Failure, T>makes failure an explicit part of every repository and use case signature —Leftfor errors,Rightfor success.- A sealed
Failurehierarchy (ServerFailure,CacheFailure,NetworkFailure,ValidationFailure) gives exhaustive, type-checked error handling. - The repository is the one place that catches raw exceptions and converts them to typed failures.
foldcollapses anEitherinto a single value (often a UI state);mapandflatMap/bindbuild short-circuiting pipelines that stop on the firstLeft.- Reserve
throwfor unrecoverable programmer errors; useEitherfor expected, recoverable ones.
الأسئلة الشائعة
هل درس «Either وأنواع الفشل ومعالجة الأخطاء الوظيفية» مجاني؟
نعم — نص درس «Either وأنواع الفشل ومعالجة الأخطاء الوظيفية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Flutter Mobile Development، انتقل إلى CoddyKit PRO. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.
ماذا ستتعلم في «Either وأنواع الفشل ومعالجة الأخطاء الوظيفية»؟
نمذج الأخطاء القابلة للاسترداد كقيم باستخدام dartz Either وأنواع الفشل المختومة تتمرن على Flutter Mobile Development مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Flutter Mobile Development؟
لا تُشترط خبرة سابقة. Flutter Mobile Development على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «Either وأنواع الفشل ومعالجة الأخطاء الوظيفية»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Flutter Mobile Development هذا؟
نعم. كل درس في Flutter Mobile Development يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- حدود طبقات المجال والبيانات والعرض
- حقن الاعتماديات باستخدام get_it وinjectable
- بنية المجلدات التي تبدأ بالميزات ومستودعات Melos متعددة الحزم
- Either وأنواع الفشل ومعالجة الأخطاء الوظيفية