Error Propagation هي آلية لنشر الخطأ لأعلى مكدس الاستدعاءات من مكان حدوثه إلى المعالج. عندما لا تستطيع دالة معالجة الخطأ بنفسها، فإنها تمرره إلى الطرف المستدعي عبر استثناء (exception) أو إعلان throws أو Return Type. التنفيذ الصحيح للانتشار أمر بالغ الأهمية لاستقرار التطبيقات المحمولة: الأخطاء غير المعالجة أو المنقولة بشكل غير صحيح تؤدي إلى الأعطال. وفقاً Apple Swift Documentation (2026)، الانتشار التلقائي عبر throws في Swift يسمح بتمرير الخطأ إلى أي مستوى دون كود متكرر.
الملامح الرئيسية
Error Propagation هي عملية تمرير كائن الخطأ من الدالة حيث حدث إلى أعلى سلسلة الاستدعاءات إلى أقرب معالج مناسب. تخيل مكدس الاستدعاءات: ViewController يستدعي ViewModel، ViewModel يستدعي Repository، Repository يستدعي API. إذا أعاد API خطأ شبكة، فيجب أن يمر عبر Repository و ViewModel إلى ViewController الذي سيعرض رسالة للمستخدم. كل دالة وسيطة تقرر: معالجة الخطأ أو تمريره لأعلى (نشره).
هناك نهجان للانتشار: تلقائي ويدوي. مع النهج التلقائي (Swift throws، الاستثناءات المفحوصة في Java) يجبر المترجم المطور إما على معالجة الخطأ أو إعلان الانتشار في التوقيع. مع النهج اليدوي (Result Type، Kotlin Try) يمرر الخطأ كقيمة — يكتب المطور بشكل صريح كوداً لتمرير الخطأ أو تحويله. وفقاً Kotlin Result Docs (2026)، Result<T> في Kotlin غير مخصص للانتشار عبر حدود الدوال مباشرة — يجب تحويله أو معالجته على كل مستوى، مما يجعل الانتشار أكثر وعياً ولكن أيضاً أكثر إسهاباً.
يعتمد اختيار النهج على بنية التطبيق واللغة. في Swift يهيمن الانتشار التلقائي عبر throws، في Kotlin يتم استخدام مزيج من الاستثناءات (للأخطاء غير المتوقعة) وحاويات شبيهة بـ Result (للأخطاء المتوقعة). من المهم أن نفهم: الانتشار ليس هدفاً، بل ضرورة. البنية المثالية تقلل من عمق الانتشار، معالجة الأخطاء في أدنى مستوى ممكن حيث يوجد سياق كافٍ لاتخاذ قرار.
في Swift، الانتشار عبر throws تلقائي: إذا كانت الدالة A مع throws تستدعي الدالة B مع throws، و A لا تعالج خطأ B في do-catch، يتم تمرير الخطأ تلقائياً إلى مستدعي A. هذا يلغي الكود المتكرر النمطي للاستثناءات المفحوصة في Java حيث يجب إعلان throws في كل طريقة من السلسلة. تستخدم Swift مبدأ «دالة throws واحدة في السلسلة = السلسلة بأكملها تصبح throws، إذا لم تتم المعالجة في المستويات الوسيطة».
struct UserRepository {
func fetchUser(id: Int) throws -> User {
let data = try networkService.request(path: "/users/\(id)")
return try parseUser(from: data)
}
}
class UserViewModel {
let repo = UserRepository()
func loadUser(id: Int) throws -> User {
return try repo.fetchUser(id: id)
}
}
// ViewController — المعالج النهائي
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("فشل تحميل المستخدم")
}
}
سلسلة الانتشار: networkService.request -> fetchUser -> loadUser -> onButtonTap. كل دالة وسيطة موسومة بـ throws ولا تحتوي على do-catch — يتم تمرير الخطأ تلقائياً لأعلى. ViewController onButtonTap هو المعالج النهائي مع do-catch. إذا قرر ViewModel تحويل الخطأ (تغليفه في نوع آخر)، يمكنه استخدام do-catch و throw جديد. الانتشار التلقائي يقلل الكود: Repository لا يحتاج إلى معرفة كيفية معالجة الخطأ — هذه مسؤولية ViewController الذي لديه وصول إلى واجهة المستخدم لعرض رسالة للمستخدم.
في Kotlin، الانتشار عبر الاستثناءات لا يتطلب إعلان throws في التوقيع (جميع الاستثناءات غير مفحوصة). الاستثناء يصعد تلقائياً لأعلى المكدس حتى يواجه try-catch. ومع ذلك، فإن غياب throws في التوقيع يجعل الانتشار ضمنياً: لا يستطيع المطور رؤية من توقيع الدالة أنها قد ترمي استثناءً. هذا ميزة (كود أقل) وعيب (يسهل نسيان المعالجة) في نفس الوقت. تحل Kotlin هذه المشكلة من خلال الاتفاقيات والأنماط المعمارية، وليس من خلال اللغة نفسها.
class UserRepository(
private val api: ApiService,
private val db: Database
) {
suspend fun getUser(id: String): User {
return try {
api.fetchUser(id)
} catch (e: IOException) {
db.getCachedUser(id) ?: throw AppException("User unavailable")
}
}
}
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = repo.getUser(id)
_state.value = UiState.Success(user)
} catch (e: AppException) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
في Repository، الانتشار مع التحويل: عند IOException (الشبكة غير متوفرة) تحاول الدالة الحصول على البيانات المخزنة مؤقتاً من قاعدة البيانات. إذا كانت الذاكرة المؤقتة فارغة، ترمي AppException — يستمر الانتشار بنوع خطأ جديد. ViewModel يلتقط AppException ويترجمه إلى UiState.Error — الخطأ لا يذهب أبعد من ذلك، ينتهي الانتشار على مستوى طبقة UI. Kotlin Coroutines تضيف خصوصيات: الاستثناءات في launch تنتشر تلقائياً عبر CoroutineExceptionHandler، وفي async — فقط عند استدعاء await(). هذا مهم عند تصميم الانتشار في الكوروتينات — SupervisorJob يمنع إلغاء الكوروتين الأب عند خطأ في كوروتين ابن.
بديل للاستثناءات هو الانتشار عبر نوع حاوية يمرر النجاح أو الخطأ كقيمة. في هذا النهج، لا تعيد الدالة قيمة بل غلافاً: Result<T, E> في Swift، Result<T> في Kotlin، Either<L, R> في Dart (من حزمة fpdart أو dartz). الخطأ لا يفك المكدس — إنه ببساطة داخل الحاوية، والمستوى التالي يقرر ماذا يفعل به. هذا يجعل الانتشار أكثر وضوحاً وتحكماً.
data class HttpResult<out T>(
val data: T?,
val error: AppError?
) {
val isSuccess: Boolean get() = data != null
val isError: Boolean get() = error != null
}
sealed class AppError {
data class Network(val message: String) : AppError()
data class Auth(val message: String) : AppError()
}
fun fetchUser(id: String): HttpResult<User> {
return try {
val response = api.get("/users/$id")
HttpResult(data = parseUser(response), error = null)
} catch (e: IOException) {
HttpResult(data = null, error = AppError.Network("No internet"))
}
}
HttpResult<T> — حاوية بسيطة بحقول data و error. الصنف المغلق AppError يحدد أنواع الأخطاء (Network, Auth). الدالة fetchUser تعيد HttpResult، الانتشار لا يتطلب فك المكدس — الطرف المستدعي يتحقق ببساطة من isSuccess/isError. هذا النهج مفيد بشكل خاص في Clean Architecture، حيث يمكن لكل طبقة (data, domain, presentation) تحويل الخطأ: IOError -> DomainError -> UiError. الانتشار عبر الحاوية يجعل هذه التحويلات صريحة وقابلة للاختبار، على عكس الاستثناءات حيث سلسلة التحويلات غير مرئية في توقيعات الدوال.
أحد القرارات الرئيسية في تصميم معالجة الأخطاء هو الاختيار بين الانتشار (التمرير لأعلى) والمعالجة (المعالجة هنا). قاعدة القرار: عالج الخطأ على المستوى الذي يوجد فيه سياق كافٍ لإجراء ذي معنى. إذا كان لديك وصول إلى واجهة المستخدم — اعرض رسالة للمستخدم. إذا كان لديك وصول إلى الذاكرة المؤقتة — حاول الاسترداد. إذا لم يكن لديك أي منهما — انشر.
| السيناريو | الإجراء | المبرر |
|---|---|---|
| خطأ شبكة في Repository | انشر | Repository لا يعرف إذا كان المستخدم يريد إعادة المحاولة |
| خطأ تحليل في Repository | عالج (أعد القيمة الافتراضية) | Repository يعرف التنسيق، يمكنه إعادة قيمة احتياطية |
| مهلة في ViewModel | عالج (UiState.Error) | ViewModel يدير UiState، يعرف كيفية ترجمة الخطأ |
| خطأ تفويض في Interceptor | عالج (تحديث الرمز) | Interceptor لديه وصول إلى الرموز ويمكنه استعادة الجلسة |
| خطأ غير معروف في UseCase | انشر | UseCase ليس لديه سياق UI — فقط منطق الأعمال |
القاعدة الذهبية: أقل انتشار، أقصى معالجة في المستويات الدنيا. إذا كان Repository يمكنه الاسترداد من الذاكرة المؤقتة — يجب أن يفعل ذلك دون تمرير الخطأ لأعلى. إذا كان ViewModel يمكنه إظهار Snackbar — فليظهره، دون طلب كود إضافي من ViewController. كل مستوى انتشار يزيد الاقتران ويعقد الاختبار. وفقاً Google Android Architecture Guide (2026)، يوصى بتقليل الانتشار عبر حدود الطبقات باستخدام صنف مغلق UiState لتمثيل جميع الحالات الممكنة (Loading, Success, Error) على مستوى ViewModel، وعدم تمرير الاستثناءات مباشرة إلى طبقة UI.
الانتشار غير الصحيح هو مصدر أخطاء يصعب اكتشافها في التطبيقات المحمولة. دعنا ننظر في خمس مشكلات رئيسية يواجهها المطورون وطرق حلها.
المشكلة الأكثر شيوعاً: أثناء الانتشار، يتم التقاط الاستثناء وتسجيله ورمي استثناء جديد بدون الاستثناء الأصلي. يفقد المطور StackTrace ولا يمكنه فهم أين حدث الخطأ بالضبط. في Swift، استخدم تسلسل الأخطاء: throw MyError(context: originalError). في Kotlin: throw AppException(cause = originalException). في Dart: throw AppException(message, originalException). لا تنشئ أبداً استثناءً جديداً دون تمرير cause/underlyingError.
catch (e: Exception) { /* لا شيء */ } هو نمط مكافح يؤدي إلى استمرار التطبيق في العمل بحالة غير صحيحة. إذا كنت متأكداً من أنه يمكن تجاهل الخطأ — أضف تعليقاً مع المبرر. في Swift، استخدم try? للتجاهل الاختياري (خطأ -> nil). في Kotlin — Result<T>.onFailure { /* سجل */ }. لا تبتلع الاستثناءات دون تسجيل.
إذا مر خطأ عبر 5+ مستويات دون معالجة، تحتاج البنية إلى إعادة نظر. كل مستوى انتشار هو اعتماد على توقيع throws للدوال الأساسية. الحل: استخدم حاويات الفشل (sealed class Result { Success, Error }) على حدود الطبقات لجعل الانتشار صريحاً ومحدوداً. كلما كانت سلسلة الانتشار أقصر، كان اختبار الكود وتصحيحه أسهل.
في Kotlin Coroutines، الاستثناء في launch بشكل افتراضي يلغي الكوروتين الأب وجميع siblings (أبناء نفس النطاق). إذا فشلت مهمة واحدة من 10 مهام متوازية، سيتم إلغاء الـ 9 الأخرى، وهو أمر غير مرغوب فيه غالباً. استخدم SupervisorJob أو supervisorScope لعزل الأخطاء: خطأ في ابن واحد لا يلغي siblings. ViewModelScope يستخدم SupervisorJob افتراضياً، مما يحمي من هذه المشكلة في Android.
في API القائمة على callback، غالباً ما يمرر الخطأ كمعامل للاستدعاء. إذا لم يعالج callback الخطأ (أو عالجه بشكل غير صحيح)، يصبح الانتشار ضمنياً ويسهل فقده. الحل: هاجر إلى async/await (Swift) أو الكوروتينات (Kotlin)، حيث يعمل الانتشار عبر آليات try-catch القياسية. إذا كان callback لا مفر منه — استخدم Either<Error, T> أو Result<T> للإجبار على معالجة كلا الحالتين.
الأسئلة الشائعة
Throw هو إجراء لمرة واحدة لرمي استثناء. Error Propagation هو العملية الكاملة لتمرير خطأ عبر عدة مستويات من المكدس، من throw إلى catch. يتضمن الانتشار throw، والتمرير التلقائي أو اليدوي عبر الدوال الوسيطة، والمعالجة النهائية. إنه مفهوم أوسع يصف دورة حياة الخطأ.
استخدم كائنات mock التي ترمي استثناءات في سيناريوهات محددة. تحقق من أن الدالة تنشر أو تعالج الخطأ بشكل صحيح عبر assertThrows (Kotlin/JUnit) أو XCTAssertThrowsError (Swift/XCTest). للانتشار المستند إلى Result، تحقق من isSuccess/isError والقيم في كلتا الحالتين.
الانتشار عبر Result مفضل لـ الأخطاء المتوقعة (بيانات غير صالحة، قواعد العمل) ضمن حد معماري واحد. الاستثناءات أفضل لـ الأخطاء غير المتوقعة (فقدان الشبكة، أخطاء الإدخال/الإخراج) التي يجب معالجتها على مستوى عالٍ. النتيجة مع خطأ لا تقطع تدفق التنفيذ، الاستثناء يقطعه.
في Kotlin Coroutines، الاستثناء في launch ينتشر تلقائياً عبر CoroutineScope مع إلغاء siblings. استخدم supervisorScope أو SupervisorJob للعزل: خطأ في كوروتين واحد لا يلغي الآخرين. بالنسبة لـ async، يجب معالجة الخطأ صراحة عبر try-catch عند استدعاء await()، وإلا سيتم ابتلاعه.
المعالج النهائي المثالي هو طبقة UI (ViewController, Fragment/Composable). فقط هي لديها وصول إلى واجهة المستخدم ويمكنها عرض رسالة أو Snackbar أو حوار. الطبقات الوسيطة (Repository, UseCase, ViewModel) تنشر الخطأ، محولة إياه إذا لزم الأمر إلى نوع domain أكثر تجريداً.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا