گسترش خطا: چیست، مکانیسم انتشار خطا و نحوه کار در توسعه موبایل

نویسنده: IT Sectr منتشر شده: 2026-05-26 زمان مطالعه: 9 دقیقه

گسترش خطا — مکانیسم انتشار خطا به بالای پشته فراخوانی از محل وقوع تا پردازشگر. هنگامی که یک تابع نمی‌تواند خطا را به طور مستقل پردازش کند، آن را از طریق استثنا (exception)، اعلان throws یا نوع بازگشتی به طرف فراخواننده منتقل می‌کند. پیاده‌سازی صحیح انتشار برای پایداری برنامه‌های موبایل حیاتی است: خطاهای پردازش‌نشده یا نادرست منتقل‌شده منجر به کرش می‌شوند. طبق Apple Swift Documentation (2026)، انتشار خودکار از طریق throws در Swift امکان انتقال خطا به هر سطحی را بدون کد boilerplate فراهم می‌کند.

نکات اصلی

  • گسترش خطا — انتقال خطا از محل وقوع به بالای پشته به پردازشگر، با عبور از توابع میانی
  • انتشار خودکار از طریق throws در Swift خطا را بدون کد صریح در هر سطح پشته منتقل می‌کند
  • انتشار دستی در Kotlin و Dart نیاز به try-catch صریح یا انتقال در کانتینر Result در هر سطح دارد
  • استثناهای بررسی‌شده در Java انتشار را از طریق throws در امضا اجباری می‌کنند، بررسی‌نشده‌ها امکان نادیده گرفتن را می‌دهند
  • نوع Result — جایگزینی برای استثناها، جایی که خطا به عنوان مقدار بدون باز کردن پشته منتقل می‌شود

گسترش خطا چیست؟

گسترش خطا — فرآیند انتقال شیء خطا از تابعی که در آن رخ داده به بالای زنجیره فراخوانی تا نزدیک‌ترین پردازشگر مناسب است. پشته فراخوانی را تصور کنید: ViewController، ViewModel را فراخوانی می‌کند، ViewModel، Repository را فراخوانی می‌کند، Repository، API را فراخوانی می‌کند. اگر API خطای شبکه را برگرداند، باید از طریق Repository و ViewModel به ViewController برسد که پیام را به کاربر نشان دهد. هر تابع میانی تصمیم می‌گیرد: خطا را پردازش کند یا بیشتر منتقل کند (گسترش دهد).

دو رویکرد برای انتشار وجود دارد: خودکار و دستی. در رویکرد خودکار (Swift throws، Java checked exceptions) کامپایلر برنامه‌نویس را مجبور می‌کند یا خطا را پردازش کند یا انتشار را در امضا اعلام کند. در رویکرد دستی (نوع Result، 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 منتقل می‌شود. این کار کد boilerplate مشخصه Java checked exceptions را حذف می‌کند، جایی که 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 است که برای نشان دادن پیام به کاربر به UI دسترسی دارد.

انتشار از طریق استثناها در Kotlin

در Kotlin انتشار از طریق استثناها نیازی به اعلان throws در امضا ندارد (همه استثناها بررسی‌نشده هستند). استثنا به طور خودکار در پشته بالا می‌رود تا به try-catch برسد. با این حال، نبود throws در امضا انتشار را ضمنی می‌کند: برنامه‌نویس از امضای تابع نمی‌بیند که ممکن است استثنا پرتاب کند. این هم مزیت (boilerplate کمتر) و هم عیب (فراموش کردن پردازش آسان‌تر) است. 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

جایگزین استثناها — انتشار از طریق نوع-کانتینر که موفقیت یا خطا را به عنوان مقدار منتقل می‌کند. در این رویکرد، تابع مقدار را برنمی‌گرداند، بلکه یک پوشش برمی‌گرداند: 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. Sealed class AppError انواع خطاها را تعریف می‌کند (Network, Auth). تابع fetchUser HttpResult را برمی‌گرداند، انتشار نیازی به باز کردن پشته ندارد — طرف فراخواننده به سادگی isSuccess/isError را بررسی می‌کند. این رویکرد به ویژه در Clean Architecture مفید است، جایی که هر لایه (data, domain, presentation) می‌تواند خطا را تبدیل کند: IOError -> DomainError -> UiError. انتشار از طریق کانتینر این تبدیل‌ها را صریح و قابل آزمایش می‌کند، برخلاف استثناها که زنجیره تبدیل‌ها در امضای توابع قابل مشاهده نیست.

انتشار در مقابل پردازش: چه زمانی منتقل کنیم، چه زمانی پردازش کنیم

یکی از تصمیمات کلیدی در طراحی پردازش خطا، انتخاب بین انتشار (انتقال به بالا) و پردازش (پردازش در اینجا) است. قانون تصمیم‌گیری: خطا را در سطحی پردازش کن که زمینه کافی برای اقدام معنادار وجود دارد. اگر به UI دسترسی داری — پیام را به کاربر نشان بده. اگر به کش دسترسی داری — سعی کن بازیابی کنی. اگر هیچکدام نیست — انتشار بده.

سناریواقدامدلیل
خطای شبکه در RepositoryانتشارRepository نمی‌داند کاربر می‌خواهد درخواست را تکرار کند یا نه
خطای تجزیه در Repositoryپردازش (مقدار پیش‌فرض برگردان)Repository فرمت را می‌شناسد، می‌تواند مقدار جایگزین برگرداند
تایم‌اوت در ViewModelپردازش (UiState.Error)ViewModel UiState را مدیریت می‌کند، می‌داند چگونه خطا را ترجمه کند
خطای احراز هویت در Interceptorپردازش (رفرش توکن)Interceptor به توکن‌ها دسترسی دارد و می‌تواند جلسه را بازیابی کند
خطای ناشناخته در UseCaseانتشارUseCase زمینه UI ندارد — فقط منطق تجاری

قاعده طلایی: حداقل انتشار، حداکثر پردازش در سطوح پایین‌تر. اگر Repository می‌تواند از کش بازیابی کند — باید این کار را انجام دهد، بدون انتقال خطا به بالا. اگر ViewModel می‌تواند Snackbar نشان دهد — بگذار نشان دهد، بدون نیاز به کد اضافی از ViewController. هر سطح انتشار وابستگی را افزایش می‌دهد و آزمایش را دشوارتر می‌کند. طبق Google Android Architecture Guide (2026)، توصیه می‌شود انتشار از طریق مرزهای لایه‌ها با استفاده از sealed class UiState برای نمایش همه حالت‌های ممکن (Loading, Success, Error) در سطح ViewModel به حداقل برسد و استثناها مستقیماً به لایه UI منتقل نشوند.

مشکلات و ضدالگوهای گسترش خطا

انتشار نادرست منبع اشکالاتی است که به سختی در برنامه‌های موبایل یافت می‌شوند. بیایید پنج مشکل اصلی را که برنامه‌نویسان با آنها مواجه می‌شوند و راه‌حل‌هایشان را بررسی کنیم.

از دست رفتن زمینه خطا

رایج‌ترین مشکل: در هنگام انتشار، استثنا گرفته می‌شود، لاگ می‌شود و استثنای جدیدی بدون استثنای اصلی پرتاب می‌شود. برنامه‌نویس StackTrace را از دست می‌دهد و نمی‌تواند بفهمد خطا دقیقاً کجا رخ داده است. در Swift از زنجیره‌سازی خطا استفاده کنید: throw MyError(context: originalError). در Kotlin: throw AppException(cause = originalException). در Dart: throw AppException(message, originalException). هرگز استثنای جدیدی بدون انتقال علت/خطای اصلی ایجاد نکنید.

نادیده گرفتن خطا (catch خالی)

catch (e: Exception) { /* هیچ */ } — ضدالگویی که باعث می‌شود برنامه در وضعیت نادرست به کار ادامه دهد. اگر مطمئن هستید که خطا قابل نادیده گرفتن است — یک نظر با توضیح اضافه کنید. در Swift برای نادیده گرفتن اختیاری از try? استفاده کنید (خطا -> nil). در Kotlin — Result<T>.onFailure { /* log */ }. استثناها را بدون لاگ کردن خفه نکنید.

عمق بیش از حد انتشار

اگر خطا از 5+ سطح بدون پردازش عبور کند، معماری نیاز به بازبینی دارد. هر سطح انتشار یک وابستگی به امضای throws توابع پایین‌دستی است. راه‌حل: از کانتینرهای Failure (sealed class Result { Success, Error }) در مرزهای لایه‌ها استفاده کنید تا انتشار صریح و محدود شود. هرچه زنجیره انتشار کوتاه‌تر باشد، آزمایش و اشکال‌زدایی کد آسان‌تر است.

انتشار در کوروتین‌ها بدون SupervisorJob

در Kotlin Coroutines، استثنا در launch به طور پیش‌فرض کوروتین والد و همه siblings (فرزندان همان scope) را لغو می‌کند. اگر یکی از 10 وظیفه موازی شکست بخورد، 9 task دیگر لغو می‌شوند که اغلب نامطلوب است. از SupervisorJob یا supervisorScope برای جداسازی خطاها استفاده کنید: خطا در یک child siblings را لغو نمی‌کند. ViewModelScope به طور پیش‌فرض از SupervisorJob استفاده می‌کند که در Android از این مشکل جلوگیری می‌کند.

انتشار از طریق callback بدون پردازش

در API مبتنی بر callback، خطا اغلب به عنوان پارامتر callback منتقل می‌شود. اگر callback خطا را پردازش نکند (یا نادرست پردازش کند)، انتشار ضمنی می‌شود و به راحتی گم می‌شود. راه‌حل: به async/await (Swift) یا کوروتین‌ها (Kotlin) مهاجرت کنید، جایی که انتشار از طریق مکانیسم‌های استاندارد try-catch کار می‌کند. اگر callback اجتناب‌ناپذیر است — از Either<Error, T> یا Result<T> برای پردازش اجباری هر دو حالت استفاده کنید.

سوالات متداول

تفاوت گسترش خطا با throw چیست؟

Throw — یک اقدام یک‌باره برای پرتاب استثنا است. گسترش خطا — کل فرآیند انتقال خطا از طریق چندین سطح پشته، از throw تا catch است. انتشار شامل throw، انتقال خودکار یا دستی از طریق توابع میانی و پردازش نهایی است. این مفهوم گسترده‌تری است که چرخه حیات خطا را توصیف می‌کند.

چگونه گسترش خطا را آزمایش کنیم؟

از اشیاء 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) خطا را منتشر می‌کنند و در صورت لزوم آن را به نوع دامنه انتزاعی‌تر تبدیل می‌کنند.

خلاصه

  • گسترش خطا — انتقال خطا به بالای پشته از محل وقوع به پردازشگر از طریق استثناها یا کانتینرهای Result
  • انتشار خودکار در Swift از طریق throws نیازی به کد در سطوح میانی ندارد — خطا خود به خود بالا می‌رود
  • انتشار دستی در Kotlin از طریق try-catch و throw صریح در هر سطح، پردازش را آگاهانه اما پرگفتار می‌کند
  • نوع Result خطا را به عنوان مقدار بدون باز کردن پشته منتقل می‌کند، برای خطاهای مورد انتظار در Clean Architecture مناسب است
  • قاعده پردازش: در سطح با زمینه پردازش کن (UI)، از سطوح بدون زمینه انتشار بده (domain, data)
  • ضدالگوها: catch خالی، از دست دادن علت در تبدیل، عمق بیش از حد انتشار، نادیده گرفتن SupervisorJob
  • خطاهای نوع‌بندی شده (sealed class / enum) برای هر لایه طراحی کنید و هنگام عبور از مرزهای لایه‌ها آنها را تبدیل کنید

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید