Error Propagation: ما هو، آلية انتشار الأخطاء وكيف يعمل في تطوير التطبيقات المحمولة

المؤلف: IT Sectr نُشر: 2026-05-26 وقت القراءة: 9 دق

Error Propagation هي آلية لنشر الخطأ لأعلى مكدس الاستدعاءات من مكان حدوثه إلى المعالج. عندما لا تستطيع دالة معالجة الخطأ بنفسها، فإنها تمرره إلى الطرف المستدعي عبر استثناء (exception) أو إعلان throws أو Return Type. التنفيذ الصحيح للانتشار أمر بالغ الأهمية لاستقرار التطبيقات المحمولة: الأخطاء غير المعالجة أو المنقولة بشكل غير صحيح تؤدي إلى الأعطال. وفقاً Apple Swift Documentation (2026)، الانتشار التلقائي عبر throws في Swift يسمح بتمرير الخطأ إلى أي مستوى دون كود متكرر.

الملامح الرئيسية

  • Error Propagation — تمرير الخطأ من مكان حدوثه لأعلى المكدس إلى معالج، متجاوزاً الدوال الوسيطة
  • الانتشار التلقائي عبر throws في Swift يمرر الخطأ بدون كود صريح على كل مستوى من المكدس
  • الانتشار اليدوي في Kotlin و Dart يتطلب try-catch صريح أو تمرير في حاوية Result على كل مستوى
  • الاستثناءات المفحوصة في Java تفرض الانتشار عبر throws في التوقيع، وغير المفحوصة يمكن تجاهلها
  • Result Type — بديل للاستثناءات حيث يمرر الخطأ كقيمة دون فك المكدس

ما هو Error Propagation؟

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 (للأخطاء المتوقعة). من المهم أن نفهم: الانتشار ليس هدفاً، بل ضرورة. البنية المثالية تقلل من عمق الانتشار، معالجة الأخطاء في أدنى مستوى ممكن حيث يوجد سياق كافٍ لاتخاذ قرار.

الانتشار عبر Throws في Swift

في Swift، الانتشار عبر throws تلقائي: إذا كانت الدالة A مع throws تستدعي الدالة B مع throws، و A لا تعالج خطأ B في do-catch، يتم تمرير الخطأ تلقائياً إلى مستدعي A. هذا يلغي الكود المتكرر النمطي للاستثناءات المفحوصة في Java حيث يجب إعلان throws في كل طريقة من السلسلة. تستخدم Swift مبدأ «دالة throws واحدة في السلسلة = السلسلة بأكملها تصبح throws، إذا لم تتم المعالجة في المستويات الوسيطة».

swift
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

في Kotlin، الانتشار عبر الاستثناءات لا يتطلب إعلان throws في التوقيع (جميع الاستثناءات غير مفحوصة). الاستثناء يصعد تلقائياً لأعلى المكدس حتى يواجه try-catch. ومع ذلك، فإن غياب throws في التوقيع يجعل الانتشار ضمنياً: لا يستطيع المطور رؤية من توقيع الدالة أنها قد ترمي استثناءً. هذا ميزة (كود أقل) وعيب (يسهل نسيان المعالجة) في نفس الوقت. تحل Kotlin هذه المشكلة من خلال الاتفاقيات والأنماط المعمارية، وليس من خلال اللغة نفسها.

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 Type

بديل للاستثناءات هو الانتشار عبر نوع حاوية يمرر النجاح أو الخطأ كقيمة. في هذا النهج، لا تعيد الدالة قيمة بل غلافاً: Result<T, E> في Swift، Result<T> في Kotlin، Either<L, R> في Dart (من حزمة fpdart أو dartz). الخطأ لا يفك المكدس — إنه ببساطة داخل الحاوية، والمستوى التالي يقرر ماذا يفعل به. هذا يجعل الانتشار أكثر وضوحاً وتحكماً.

kotlin
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.

مشكلات وأنماط مكافحة لـ Error Propagation

الانتشار غير الصحيح هو مصدر أخطاء يصعب اكتشافها في التطبيقات المحمولة. دعنا ننظر في خمس مشكلات رئيسية يواجهها المطورون وطرق حلها.

فقدان سياق الخطأ

المشكلة الأكثر شيوعاً: أثناء الانتشار، يتم التقاط الاستثناء وتسجيله ورمي استثناء جديد بدون الاستثناء الأصلي. يفقد المطور StackTrace ولا يمكنه فهم أين حدث الخطأ بالضبط. في Swift، استخدم تسلسل الأخطاء: throw MyError(context: originalError). في Kotlin: throw AppException(cause = originalException). في Dart: throw AppException(message, originalException). لا تنشئ أبداً استثناءً جديداً دون تمرير cause/underlyingError.

تجاهل الخطأ (catch فارغ)

catch (e: Exception) { /* لا شيء */ } هو نمط مكافح يؤدي إلى استمرار التطبيق في العمل بحالة غير صحيحة. إذا كنت متأكداً من أنه يمكن تجاهل الخطأ — أضف تعليقاً مع المبرر. في Swift، استخدم try? للتجاهل الاختياري (خطأ -> nil). في Kotlin — Result<T>.onFailure { /* سجل */ }. لا تبتلع الاستثناءات دون تسجيل.

عمق الانتشار المفرط

إذا مر خطأ عبر 5+ مستويات دون معالجة، تحتاج البنية إلى إعادة نظر. كل مستوى انتشار هو اعتماد على توقيع throws للدوال الأساسية. الحل: استخدم حاويات الفشل (sealed class Result { Success, Error }) على حدود الطبقات لجعل الانتشار صريحاً ومحدوداً. كلما كانت سلسلة الانتشار أقصر، كان اختبار الكود وتصحيحه أسهل.

الانتشار في الكوروتينات بدون SupervisorJob

في Kotlin Coroutines، الاستثناء في launch بشكل افتراضي يلغي الكوروتين الأب وجميع siblings (أبناء نفس النطاق). إذا فشلت مهمة واحدة من 10 مهام متوازية، سيتم إلغاء الـ 9 الأخرى، وهو أمر غير مرغوب فيه غالباً. استخدم SupervisorJob أو supervisorScope لعزل الأخطاء: خطأ في ابن واحد لا يلغي siblings. ViewModelScope يستخدم SupervisorJob افتراضياً، مما يحمي من هذه المشكلة في Android.

الانتشار عبر callback بدون معالجة

في API القائمة على callback، غالباً ما يمرر الخطأ كمعامل للاستدعاء. إذا لم يعالج callback الخطأ (أو عالجه بشكل غير صحيح)، يصبح الانتشار ضمنياً ويسهل فقده. الحل: هاجر إلى async/await (Swift) أو الكوروتينات (Kotlin)، حيث يعمل الانتشار عبر آليات try-catch القياسية. إذا كان callback لا مفر منه — استخدم Either<Error, T> أو Result<T> للإجبار على معالجة كلا الحالتين.

الأسئلة الشائعة

كيف يختلف Error Propagation عن throw؟

Throw هو إجراء لمرة واحدة لرمي استثناء. Error Propagation هو العملية الكاملة لتمرير خطأ عبر عدة مستويات من المكدس، من throw إلى catch. يتضمن الانتشار throw، والتمرير التلقائي أو اليدوي عبر الدوال الوسيطة، والمعالجة النهائية. إنه مفهوم أوسع يصف دورة حياة الخطأ.

كيفية اختبار Error Propagation؟

استخدم كائنات mock التي ترمي استثناءات في سيناريوهات محددة. تحقق من أن الدالة تنشر أو تعالج الخطأ بشكل صحيح عبر assertThrows (Kotlin/JUnit) أو XCTAssertThrowsError (Swift/XCTest). للانتشار المستند إلى Result، تحقق من isSuccess/isError والقيم في كلتا الحالتين.

متى يكون الانتشار عبر Result أفضل من الاستثناءات؟

الانتشار عبر Result مفضل لـ الأخطاء المتوقعة (بيانات غير صالحة، قواعد العمل) ضمن حد معماري واحد. الاستثناءات أفضل لـ الأخطاء غير المتوقعة (فقدان الشبكة، أخطاء الإدخال/الإخراج) التي يجب معالجتها على مستوى عالٍ. النتيجة مع خطأ لا تقطع تدفق التنفيذ، الاستثناء يقطعه.

كيفية نشر خطأ عبر الكوروتينات في Kotlin؟

في Kotlin Coroutines، الاستثناء في launch ينتشر تلقائياً عبر CoroutineScope مع إلغاء siblings. استخدم supervisorScope أو SupervisorJob للعزل: خطأ في كوروتين واحد لا يلغي الآخرين. بالنسبة لـ async، يجب معالجة الخطأ صراحة عبر try-catch عند استدعاء await()، وإلا سيتم ابتلاعه.

أي مستوى يجب أن يكون المعالج النهائي للخطأ؟

المعالج النهائي المثالي هو طبقة UI (ViewController, Fragment/Composable). فقط هي لديها وصول إلى واجهة المستخدم ويمكنها عرض رسالة أو Snackbar أو حوار. الطبقات الوسيطة (Repository, UseCase, ViewModel) تنشر الخطأ، محولة إياه إذا لزم الأمر إلى نوع domain أكثر تجريداً.

الخلاصة

  • Error Propagation — تمرير خطأ لأعلى المكدس من مكان حدوثه إلى معالج عبر الاستثناءات أو حاويات Result
  • الانتشار التلقائي في Swift عبر throws لا يتطلب كوداً في المستويات الوسيطة — الخطأ يصعد بنفسه
  • الانتشار اليدوي في Kotlin عبر try-catch صريح و throw على كل مستوى يجعل المعالجة واعية ولكن مطولة
  • Result Type يمرر الخطأ كقيمة دون فك المكدس، مناسب للأخطاء المتوقعة في Clean Architecture
  • قاعدة المعالجة: عالج على المستوى مع سياق (UI)، انشر عبر المستويات بدون سياق (domain, data)
  • أنماط مكافحة: catch فارغ، فقدان cause أثناء التحويل، عمق انتشار مفرط، تجاهل SupervisorJob
  • صمم أخطاء مطبوعة (sealed class / enum) لكل طبقة وحولها عند عبور حدود الطبقات

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا