Dart: معالجة الاستثناءات في Dart
آخر تحديث: 2026-08-26
معالجة الاستثناءات هي شبكة الأمان لكودك — بدونها، خطأ واحد يمكن أن يُنهي النظام بأكمله.
1. ما ستتعلمه
- الفرق الدلالي بين Exception و Error
- الصياغة الكاملة لـ try / on / catch / finally
- فئات الاستثناءات المخصصة وتصميم رموز الأخطاء
- rethrow وسلسلة الاستثناءات
- سيناريو بوب: استثناء تحليل الملف في خط أنابيب البيانات ومعالجة مهلة الشبكة
2. قصة مطور حقيقي
(1) نقطة الألم: الاستثناءات غير المعالجة تُسقط المعالجة الدفعية في منتصفها
أثناء معالجة ملايين الطلبات، تعطل خط أنابيب البيانات الخاص ببوب بسبب سطر CSV مشوه تسبب في FormatException. فُقدت جميع السجلات الـ 800,000 التي تمت معالجتها، مما تطلب إعادة التشغيل بالكامل. والأسوأ أن رسالة الخطأ كانت تعرض فقط "FormatException" بدون رقم سطر أو سياق، واستغرق بوب 4 ساعات لتحديد المشكلة.
(2) حل معالجة الاستثناءات
استخدم try-on-catch-finally لالتقاط استثناءات محددة، وفئات استثناءات مخصصة لحمل معلومات السياق، و finally لضمان تحرير الموارد.
try {
final records = await parseCsvFile(path);
await processRecords(records);
} on FormatException catch (e) {
log.error('CSV parse error: ${e.message} at line ${e.offset}');
// تخطي السجلات المشوهة، متابعة المعالجة
} on TimeoutException {
log.error('API timeout, retrying...');
await retryWithBackoff();
} finally {
await closeResources();
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
(3) الفوائد
- خطأ سجل واحد لم يعد يتسبب في فشل الدفعة بأكملها
- الاستثناءات المخصصة تحمل السياق مثل أرقام الأسطر والحقول، مما يقلل وقت تحديد الموقع من 4 ساعات إلى 5 دقائق
finallyيضمن تحرير مقابض الملفات واتصالات الشبكة دائماً
3. Exception مقابل Error
(1) الفرق الدلالي
flowchart TD A[Throwable] --> B[Error<br/>قابل للاسترداد: لا] A --> C[Exception<br/>قابل للاسترداد: نعم] B --> B1[OutOfMemoryError] B --> B2[StackOverflowError] C --> C1[FormatException] C --> C2[TimeoutException] C --> C3[IOException] C --> C4[استثناء مخصص]
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
| الجانب | Error | Exception |
|---|---|---|
| قابلية الاسترداد | غير قابل للاسترداد | قابل للاسترداد |
| هل يجب التقاطه | لا | نعم |
| ناتج عن | VM / Runtime | كود التطبيق |
| مثال | StackOverflowError | FormatException |
4. try / on / catch / finally
(1) الصياغة الكاملة
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: try-catch الأساسي
void main() {
try {
final result = int.parse('abc');
print(result);
} on FormatException catch (e) {
print('Format error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: الفرق بين on و catch
void main() {
// on - يلتقط نوعاً محدداً، بدون الوصول إلى كائن الاستثناء
try {
int.parse('not a number');
} on FormatException {
print('Caught FormatException (no details needed)');
}
// on + catch - يلتقط نوعاً محدداً مع الوصول إلى الاستثناء
try {
int.parse('not a number');
} on FormatException catch (e) {
print('Caught: ${e.message}');
}
// catch مع تتبع المكدس
try {
int.parse('not a number');
} on FormatException catch (e, stackTrace) {
print('Error: $e');
print('Stack: $stackTrace');
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: كتلة finally
import 'dart:io';
void main() async {
File? file;
try {
file = File('orders.csv');
final content = await file.readAsString();
print('Read ${content.length} characters');
} on FileSystemException catch (e) {
print('File error: ${e.message}');
} finally {
// يتم تنفيذه دائماً - حتى في حالة return أو throw
print('Cleanup: file handle released');
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
| البند | الغرض | يمكن تكراره | الترتيب |
|---|---|---|---|
try |
يلف الكود الذي قد يرمي استثناءً | 1 | أولاً |
on Type |
يلتقط نوعاً محدداً | متعدد | بعد try |
catch (e) |
يلتقط أي استثناء | 1 | بعد on |
finally |
يُنفذ دائماً | 1 | أخيراً |
5. فئات الاستثناءات المخصصة
(1) مبادئ تصميم فئات الاستثناءات
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: فئات الاستثناءات المخصصة
// الاستثناء الأساسي لخط أنابيب البيانات
class PipelineException implements Exception {
final String message;
final String? source;
final int? lineNumber;
PipelineException(this.message, {this.source, this.lineNumber});
@override
String toString() => 'PipelineException: $message'
'${source != null ? " (source: $source)" : ""}'
'${lineNumber != null ? " at line $lineNumber" : ""}';
}
// أنواع استثناءات محددة
class DataFormatException extends PipelineException {
final String fieldName;
final String invalidValue;
DataFormatException({
required this.fieldName,
required this.invalidValue,
required super.message,
super.source,
super.lineNumber,
});
@override
String toString() => 'DataFormatException: $message '
'(field: $fieldName, value: "$invalidValue")';
}
class NetworkTimeoutException extends PipelineException {
final Duration timeout;
final String endpoint;
NetworkTimeoutException({
required this.timeout,
required this.endpoint,
super.message = 'Request timed out',
}) : super(message);
@override
String toString() => 'NetworkTimeout: ${timeout.inSeconds}s '
'timeout on $endpoint';
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: تصميم رمز الخطأ
enum ErrorCode {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network request timed out'),
authFailed('E004', 'Authentication failed'),
rateLimitExceeded('E005', 'Rate limit exceeded');
final String code;
final String description;
const ErrorCode(this.code, this.description);
}
class CodedException extends PipelineException {
final ErrorCode errorCode;
CodedException(this.errorCode, {String? detail})
: super('${errorCode.code}: ${errorCode.description}'
'${detail != null ? " - $detail" : ""}');
@override
String toString() => '[$errorCode] $message';
}
void main() {
try {
throw CodedException(ErrorCode.invalidFormat, detail: 'amount field is not a number');
} on CodedException catch (e) {
print(e); // [ErrorCode.invalidFormat] E002: Invalid data format - amount field is not a number
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
6. rethrow وسلسلة الاستثناءات
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: rethrow
double parseAmount(String input) {
try {
return double.parse(input);
} on FormatException catch (e) {
// سجّل وأعد الرمي - لا تبتلع الاستثناء
print('Failed to parse amount: "$input"');
rethrow; // يحافظ على تتبع المكدس الأصلي
}
}
void main() {
try {
final amount = parseAmount('not_a_number');
} on FormatException {
print('Caught rethrown exception');
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: سلسلة الاستثناءات
class ChainedException implements Exception {
final String message;
final Exception? innerException;
ChainedException(this.message, {this.innerException});
@override
String toString() {
var result = 'ChainedException: $message';
if (innerException != null) {
result += '\n Caused by: $innerException';
}
return result;
}
}
Future<double> fetchOrderAmount(String orderId) async {
try {
// محاكاة استدعاء API
throw FormatException('Invalid JSON response');
} on FormatException catch (e) {
throw ChainedException(
'Failed to fetch order $orderId',
innerException: e,
);
}
}
void main() async {
try {
await fetchOrderAmount('ORD-001');
} on ChainedException catch (e) {
print(e);
}
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
7. سيناريو بوب: معالجة استثناءات خط أنابيب البيانات
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
: تدفق معالجة الاستثناءات الكامل
import 'dart:async';
// استثناءات مخصصة
class PipelineException implements Exception {
final String message;
PipelineException(this.message);
@override
String toString() => 'PipelineException: $message';
}
class CsvParseException extends PipelineException {
final int lineNumber;
CsvParseException(String message, this.lineNumber) : super(message);
@override
String toString() => 'CsvParseException: $message (line $lineNumber)';
}
// محلل CSV آمن مع معالجة الاستثناءات
List<Map<String, String>> parseCsv(String content) {
final lines = content.split('\n');
if (lines.isEmpty) throw PipelineException('Empty CSV content');
final headers = lines[0].split(',');
final records = <Map<String, String>>[];
for (var i = 1; i < lines.length; i++) {
final line = lines[i].trim();
if (line.isEmpty) continue;
try {
final values = line.split(',');
if (values.length != headers.length) {
throw CsvParseException(
'Column count mismatch: expected ${headers.length}, got ${values.length}',
i + 1,
);
}
final record = <String, String>{};
for (var j = 0; j < headers.length; j++) {
record[headers[j].trim()] = values[j].trim();
}
records.add(record);
} on CsvParseException {
rethrow;
} catch (e) {
throw CsvParseException('Unexpected error: $e', i + 1);
}
}
return records;
}
Future<void> processData(String csvContent) async {
List<Map<String, String>>? records;
try {
records = parseCsv(csvContent);
print('Parsed ${records.length} records');
// محاكاة استدعاء API مع مهلة
await Future.delayed(const Duration(seconds: 1));
print('Data submitted successfully');
} on CsvParseException catch (e) {
print('Parse error: $e - skipping malformed records');
} on TimeoutException catch (e) {
print('Network timeout: $e - will retry later');
} on PipelineException catch (e) {
print('Pipeline error: $e');
} finally {
print('Cleanup: resources released');
}
}
void main() async {
final csv = 'id,amount,status\nORD-001,1500,completed\nORD-002,50,pending\nORD-003,bad_data,completed';
await processData(csv);
print('\n--- Test with malformed CSV ---');
final badCsv = 'id,amount\nORD-001,1500,completed'; // عدد أعمدة خاطئ
await processData(badCsv);
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
8. المثال الكامل: معالجة بيانات متينة لخط أنابيب البيانات
// ============================================
// معالجة بيانات متينة لخط أنابيب البيانات
// معالجة استثناءات كاملة مع استثناءات مخصصة
// ============================================
import 'dart:async';
// رموز الأخطاء
enum PipelineError {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network timeout'),
validationFailed('E004', 'Validation failed');
final String code;
final String label;
const PipelineError(this.code, this.label);
}
class PipelineException implements Exception {
final PipelineError error;
final String detail;
final Exception? cause;
PipelineException(this.error, {this.detail = '', this.cause});
@override
String toString() => '[${error.code}] ${error.label}'
'${detail.isNotEmpty ? ": $detail" : ""}'
'${cause != null ? " (caused by: $cause)" : ""}';
}
// طلب مع التحقق
class Order {
final String id;
final double amount;
final String status;
Order({required this.id, required this.amount, required this.status}) {
if (id.isEmpty) {
throw PipelineException(PipelineError.validationFailed, detail: 'Order ID is empty');
}
if (amount <= 0) {
throw PipelineException(PipelineError.validationFailed, detail: 'Amount must be positive: $amount');
}
}
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status)';
}
// محلل طلبات متين
class OrderParser {
final List<PipelineException> _errors = [];
int parsed = 0;
int skipped = 0;
List<PipelineException> get errors => List.unmodifiable(_errors);
Order? tryParse(Map<String, dynamic> data) {
try {
final order = Order(
id: (data['id'] ?? '') as String,
amount: (data['amount'] as num).toDouble(),
status: (data['status'] ?? 'unknown') as String,
);
parsed++;
return order;
} on PipelineException catch (e) {
_errors.add(e);
skipped++;
return null;
} on TypeError catch (e) {
_errors.add(PipelineException(
PipelineError.invalidFormat,
detail: 'Type mismatch in record: $e',
));
skipped++;
return null;
}
}
void printReport() {
print('Parsed: $parsed, Skipped: $skipped');
if (_errors.isNotEmpty) {
print('Errors:');
for (final e in _errors) {
print(' $e');
}
}
}
}
void main() {
final rawData = <Map<String, dynamic>>[
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed'},
{'id': '', 'amount': 500.0, 'status': 'pending'}, // غير صالح: معرف فارغ
{'id': 'ORD-003', 'amount': -50.0, 'status': 'completed'}, // غير صالح: مبلغ سالب
{'id': 'ORD-004', 'amount': 'not_a_number', 'status': 'pending'}, // غير صالح: نوع خاطئ
{'id': 'ORD-005', 'amount': 3200.0, 'status': 'completed'},
];
final parser = OrderParser();
final validOrders = <Order>[];
for (final data in rawData) {
final order = parser.tryParse(data);
if (order != null) validOrders.add(order);
}
print('=== DataPipeline Processing Report ===');
parser.printReport();
print('\nValid Orders:');
for (final order in validOrders) {
print(' $order');
}
final totalRevenue = validOrders.fold<double>(0, (s, o) => s + o.amount);
print('\nTotal Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x، وقد تختلف النتائج قليلاً حسب إصدار SDK.
الإخراج:
=== DataPipeline Processing Report ===
Parsed: 3, Skipped: 2
Errors:
[E004] Validation failed: Order ID is empty
[E004] Validation failed: Amount must be positive: -50.0
[E002] Invalid data format: Type mismatch in record: ...
Valid Orders:
Order(ORD-001, $1500.00, completed)
Order(ORD-005, $3200.00, completed)
Total Revenue: $4700.00 USD
❓ أسئلة شائعة
س: هل لدى Dart استثناءات محددة (checked exceptions)؟ ج: لا. جميع الاستثناءات في Dart غير محددة (unchecked)؛ المترجم لا يفرض الإعلان عنها أو التقاطها. هذا يوفر مرونة لكنه يتطلب من المطورين التعامل مع الاستثناءات بوعي.
س: ما الفرق بين
onوcatch؟ ج:on Typeيلتقط نوع استثناء محدداً دون ربط متغير (ما لم تتم إضافةcatch).catch (e)يلتقط أي استثناء ويربطه بمتغير. عادةً، يُستخدم المزيجon Type catch (e).
س: ما فائدة
catchوrethrow؟ ج:catchيتيح لك التسجيل، تنفيذ التنظيف، إلخ، بعد التقاط الاستثناء. ثم استخدمrethrowلإعادة رمي نفس الاستثناء حتى تتمكن الطبقات العليا من معالجته.rethrowيحافظ على تتبع المكدس الأصلي، وهو أفضل منthrow e.
س: متى تُنفذ كتلة
finally؟ ج: تُنفذ كتلةfinallyدائماً، بصرف النظر عما إذا كان قد تم رمي استثناء في كتلةtry، أو ما إذا تم التقاطه، أو ما إذا كانت هناك جملةreturn. الاستثناء الوحيد هو إذا تم إنهاء البرنامج (مثلاً عبر SIGKILL).
س: ما الذي يجب أن ترث منه الاستثناءات المخصصة؟ ج: يُنصح بـ
implement Exceptionبدلاً منextend Exception.implementsأكثر مرونة، ولا يقيدك بالوراثة الأحادية. هذا هو الأسلوب الذي توصي به إرشادات Dart الرسمية.
س: هل معالجة الاستثناءات تؤثر على الأداء؟ ج: كتلة try-catch نفسها لها عبء قريب من الصفر (لا تضيف آلة Dart الافتراضية تعليمات إضافية داخل كتل try). ومع ذلك، فإن إنشاء الاستثناءات ورميها له تكلفة، ولا ينبغي استخدامها كآلية تحكم عادية في التدفق.
س: كيف أتجنب ابتلاع الاستثناءات؟ ج: كتلة
catchالفارغة هي علامة على كود سيء. على الأقل سجّل الاستثناء، أو استخدمrethrow. إذا كان يجب عليك تجاهله، استخدمcatch (_) {}وأضف تعليقاً يوضح السبب.
📖 ملخص
- الاستثناءات قابلة للاسترداد ويجب التقاطها؛ الأخطاء غير قابلة للاسترداد ولا يجب التقاطها
- صياغة try-on-catch-finally الكاملة:
onيلتقط حسب النوع،catchيربط متغيراً،finallyيُنفذ دائماً - الاستثناءات المخصصة
implement Exception، حاملة السياق (رقم السطر، الحقل، رمز الخطأ) rethrowيحافظ على تتبع المكدس الأصلي؛ سلسلة الاستثناءات تساعد في تتبع السبب الجذري- يستخدم خط أنابيب البيانات نمط
tryParse: فشل سجل واحد لا يؤثر على معالجة الدفعة بأكملها
📝 تمارين
- أساسي (صعوبة ⭐): اكتب دالة
safeParseInt(String s)تستخدمint.parseداخل كتلة try-catch. إذا فشل التحليل، أرجع0بدلاً من رمي استثناء. اختبر بـ 3 حالات. - متوسط (صعوبة ⭐⭐): عرّف فئة استثناء مخصص
DataFormatExceptionتحمل اسم الحقل والقيمة غير الصالحة ورقم السطر. اكتب دالة تحليل CSV ترمي هذا الاستثناء عند أخطاء التنسيق، والتقطها في موقع الاستدعاء لطباعة معلومات مفصلة. - تحدي (صعوبة ⭐⭐⭐): نفّذ دالة طلب HTTP مع آلية إعادة محاولة تدعم عدداً مخصصاً من المحاولات واستراتيجية تراجع. أعد المحاولة عند
TimeoutException، وبعد تجاوز عدد المحاولات، ارمِ استثناءً مجمعاً يحتوي على جميع استثناءات المحاولات الفردية.