گسترش خطا — مکانیسم انتشار خطا به بالای پشته فراخوانی از محل وقوع تا پردازشگر. هنگامی که یک تابع نمیتواند خطا را به طور مستقل پردازش کند، آن را از طریق استثنا (exception)، اعلان throws یا نوع بازگشتی به طرف فراخواننده منتقل میکند. پیادهسازی صحیح انتشار برای پایداری برنامههای موبایل حیاتی است: خطاهای پردازشنشده یا نادرست منتقلشده منجر به کرش میشوند. طبق Apple Swift Documentation (2026)، انتشار خودکار از طریق throws در Swift امکان انتقال خطا به هر سطحی را بدون کد boilerplate فراهم میکند.
نکات اصلی
گسترش خطا — فرآیند انتقال شیء خطا از تابعی که در آن رخ داده به بالای زنجیره فراخوانی تا نزدیکترین پردازشگر مناسب است. پشته فراخوانی را تصور کنید: 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مانند (برای خطاهای مورد انتظار). مهم است بدانید: انتشار هدف نیست، بلکه ضرورت است. معماری ایدهآل عمق انتشار را با پردازش خطاها در پایینترین سطح ممکن، جایی که زمینه کافی برای تصمیمگیری وجود دارد، به حداقل میرساند.
در Swift انتشار از طریق throws به طور خودکار انجام میشود: اگر تابع A با throws تابع B با throws را فراخوانی کند و A خطای B را در do-catch پردازش نکند، خطا به طور خودکار به طرف فراخواننده A منتقل میشود. این کار کد boilerplate مشخصه Java checked exceptions را حذف میکند، جایی که 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 است که برای نشان دادن پیام به کاربر به UI دسترسی دارد.
در Kotlin انتشار از طریق استثناها نیازی به اعلان throws در امضا ندارد (همه استثناها بررسینشده هستند). استثنا به طور خودکار در پشته بالا میرود تا به try-catch برسد. با این حال، نبود throws در امضا انتشار را ضمنی میکند: برنامهنویس از امضای تابع نمیبیند که ممکن است استثنا پرتاب کند. این هم مزیت (boilerplate کمتر) و هم عیب (فراموش کردن پردازش آسانتر) است. 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. 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 (e: Exception) { /* هیچ */ } — ضدالگویی که باعث میشود برنامه در وضعیت نادرست به کار ادامه دهد. اگر مطمئن هستید که خطا قابل نادیده گرفتن است — یک نظر با توضیح اضافه کنید. در Swift برای نادیده گرفتن اختیاری از try? استفاده کنید (خطا -> nil). در Kotlin — Result<T>.onFailure { /* log */ }. استثناها را بدون لاگ کردن خفه نکنید.
اگر خطا از 5+ سطح بدون پردازش عبور کند، معماری نیاز به بازبینی دارد. هر سطح انتشار یک وابستگی به امضای throws توابع پاییندستی است. راهحل: از کانتینرهای Failure (sealed class Result { Success, Error }) در مرزهای لایهها استفاده کنید تا انتشار صریح و محدود شود. هرچه زنجیره انتشار کوتاهتر باشد، آزمایش و اشکالزدایی کد آسانتر است.
در Kotlin Coroutines، استثنا در launch به طور پیشفرض کوروتین والد و همه siblings (فرزندان همان scope) را لغو میکند. اگر یکی از 10 وظیفه موازی شکست بخورد، 9 task دیگر لغو میشوند که اغلب نامطلوب است. از SupervisorJob یا supervisorScope برای جداسازی خطاها استفاده کنید: خطا در یک child siblings را لغو نمیکند. ViewModelScope به طور پیشفرض از SupervisorJob استفاده میکند که در Android از این مشکل جلوگیری میکند.
در API مبتنی بر callback، خطا اغلب به عنوان پارامتر callback منتقل میشود. اگر callback خطا را پردازش نکند (یا نادرست پردازش کند)، انتشار ضمنی میشود و به راحتی گم میشود. راهحل: به async/await (Swift) یا کوروتینها (Kotlin) مهاجرت کنید، جایی که انتشار از طریق مکانیسمهای استاندارد try-catch کار میکند. اگر callback اجتنابناپذیر است — از Either<Error, T> یا Result<T> برای پردازش اجباری هر دو حالت استفاده کنید.
سوالات متداول
Throw — یک اقدام یکباره برای پرتاب استثنا است. گسترش خطا — کل فرآیند انتقال خطا از طریق چندین سطح پشته، از 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) خطا را منتشر میکنند و در صورت لزوم آن را به نوع دامنه انتزاعیتر تبدیل میکنند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید