Error Propagation เป็นกลไกในการแพร่กระจายข้อผิดพลาดขึ้นไปตามสแต็กการเรียกจากจุดที่เกิดขึ้นไปยังตัวจัดการ เมื่อฟังก์ชันไม่สามารถจัดการข้อผิดพลาดได้ด้วยตัวเอง มันจะส่งต่อข้อผิดพลาดไปยังฝ่ายที่เรียกผ่านข้อยกเว้น (exception) การประกาศ throws หรือ Return Type การใช้งาน propagation ที่ถูกต้องเป็นสิ่งสำคัญสำหรับความเสถียรของแอปพลิเคชันมือถือ: ข้อผิดพลาดที่ไม่ถูกจัดการหรือส่งต่ออย่างไม่ถูกต้องนำไปสู่การขัดข้อง ตาม Apple Swift Documentation (2026) propagation อัตโนมัติ ผ่าน throws ใน Swift ช่วยให้ส่งข้อผิดพลาดไปยังระดับใดก็ได้โดยไม่ต้องใช้โค้ดที่ซ้ำซ้อน
ประเด็นสำคัญ
Error Propagation (การแพร่กระจายข้อผิดพลาด) คือกระบวนการส่งออบเจ็กต์ข้อผิดพลาดจากฟังก์ชันที่มันเกิดขึ้นขึ้นไปตามห่วงโซ่การเรียกไปยังตัวจัดการที่เหมาะสมที่ใกล้ที่สุด ลองนึกภาพสแต็กการเรียก: ViewController เรียก ViewModel, ViewModel เรียก Repository, Repository เรียก API ถ้า API ส่งคืนข้อผิดพลาดเครือข่าย มันต้องผ่าน Repository และ ViewModel ไปยัง ViewController ซึ่งจะแสดงข้อความให้ผู้ใช้ ฟังก์ชันกลางแต่ละตัวตัดสินใจ: จัดการข้อผิดพลาดหรือส่งต่อ (propagate)
มีสองแนวทางสำหรับ propagation: อัตโนมัติและด้วยตนเอง ด้วยแนวทางอัตโนมัติ (Swift throws, Java checked exceptions) คอมไพเลอร์บังคับให้นักพัฒนาจัดการข้อผิดพลาดหรือประกาศ propagation ในลายเซ็นต์ ด้วยแนวทางด้วยตนเอง (Result Type, Kotlin Try) ข้อผิดพลาดถูกส่งเป็นค่า — นักพัฒนาเขียนโค้ดอย่างชัดเจนเพื่อส่งหรือแปลงข้อผิดพลาด ตาม Kotlin Result Docs (2026) Result<T> ใน Kotlin ไม่ได้ออกแบบมาสำหรับ propagation โดยตรงข้ามขอบเขตฟังก์ชัน — มันต้องถูกแปลงหรือจัดการในแต่ละระดับ ซึ่งทำให้ propagation มีสติมากขึ้นแต่ก็มีความ冗长มากขึ้น
การเลือกแนวทางขึ้นอยู่กับสถาปัตยกรรมแอปพลิเคชันและภาษา ใน Swift การ propagation อัตโนมัติผ่าน throws เป็นหลัก ใน Kotlin ใช้การผสมของข้อยกเว้น (สำหรับข้อผิดพลาดที่ไม่คาดคิด) และคอนเทนเนอร์คล้าย Result (สำหรับข้อผิดพลาดที่คาดหวัง) สิ่งสำคัญที่ต้องเข้าใจ: propagation ไม่ใช่เป้าหมาย แต่เป็นความจำเป็น สถาปัตยกรรมในอุดมคติลดความลึกของ propagation ให้เหลือน้อยที่สุด จัดการข้อผิดพลาดในระดับต่ำสุดที่เป็นไปได้ซึ่งมีบริบทเพียงพอสำหรับการตัดสินใจ
ใน Swift การ propagation ผ่าน throws เป็นอัตโนมัติ: ถ้าฟังก์ชัน A ที่มี throws เรียกฟังก์ชัน B ที่มี throws และ A ไม่จัดการข้อผิดพลาดของ B ใน do-catch ข้อผิดพลาดจะถูกส่งไปยังผู้เรียกของ A โดยอัตโนมัติ สิ่งนี้กำจัดโค้ดที่ซ้ำซ้อนทั่วไปของ 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("ไม่สามารถโหลดผู้ใช้ได้")
}
}
ห่วงโซ่ propagation: networkService.request -> fetchUser -> loadUser -> onButtonTap ฟังก์ชันกลางแต่ละตัวถูกทำเครื่องหมาย throws และไม่มี do-catch — ข้อผิดพลาดถูกส่งขึ้นไปโดยอัตโนมัติ ViewController onButtonTap เป็นตัวจัดการสุดท้ายที่มี do-catch ถ้า ViewModel ตัดสินใจแปลงข้อผิดพลาด (ห่อเป็นประเภทอื่น) มันสามารถใช้ do-catch และ throw ใหม่ได้ propagation อัตโนมัติ ลดโค้ด: Repository ไม่จำเป็นต้องรู้วิธีจัดการข้อผิดพลาด — นั่นคือความรับผิดชอบของ ViewController ที่สามารถเข้าถึง UI เพื่อแสดงข้อความให้ผู้ใช้
ใน Kotlin การ propagation ผ่านข้อยกเว้นไม่ต้องการการประกาศ throws ในลายเซ็นต์ (ข้อยกเว้นทั้งหมดเป็น unchecked) ข้อยกเว้นจะถูกยกขึ้นไปตามสแต็กโดยอัตโนมัติจนกว่าจะพบ try-catch อย่างไรก็ตาม การไม่มี throws ในลายเซ็นต์ทำให้ propagation เป็นโดยนัย: นักพัฒนาไม่สามารถเห็นจากลายเซ็นต์ฟังก์ชันว่ามันอาจโยนข้อยกเว้น นี่เป็นทั้งข้อดี (โค้ดซ้ำซ้อนน้อยกว่า) และข้อเสีย (ลืมจัดการได้ง่ายกว่า) ในเวลาเดียวกัน 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 propagation ด้วยการแปลง: เมื่อ IOException (เครือข่ายไม่พร้อมใช้งาน) ฟังก์ชันพยายามรับข้อมูลที่ถูกแคชจากฐานข้อมูล ถ้าแคชว่าง มันจะโยน AppException — propagation ดำเนินต่อด้วยประเภทข้อผิดพลาดใหม่ ViewModel จับ AppException และแปลงเป็น UiState.Error — ข้อผิดพลาดไม่เดินหน้าต่อไป propagation สิ้นสุดที่ระดับเลเยอร์ UI Kotlin Coroutines เพิ่มลักษณะเฉพาะ: ข้อยกเว้นใน launch แพร่กระจายโดยอัตโนมัติผ่าน CoroutineExceptionHandler และใน async — เฉพาะเมื่อเรียก await() สิ่งนี้สำคัญเมื่อออกแบบ propagation ในคอรูน — SupervisorJob ป้องกันการยกเลิกคอรูนแม่เมื่อเกิดข้อผิดพลาดในคอรูนลูก
ทางเลือกแทนข้อยกเว้นคือการ propagation ผ่านประเภทคอนเทนเนอร์ที่ส่งความสำเร็จหรือข้อผิดพลาดเป็นค่า ในแนวทางนี้ ฟังก์ชันไม่ส่งค่ากลับแต่ส่ง wrapper: Result<T, E> ใน Swift, Result<T> ใน Kotlin, Either<L, R> ใน Dart (จากแพ็กเกจ fpdart หรือ dartz) ข้อผิดพลาดไม่คลายสแต็ก — มันแค่อยู่ในคอนเทนเนอร์ และระดับถัดไปตัดสินใจว่าจะทำอย่างไรกับมัน สิ่งนี้ทำให้ propagation ชัดเจนและควบคุมได้มากขึ้น
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, propagation ไม่ต้องการการคลายสแต็ก — ผู้เรียกเพียงแค่ตรวจสอบ isSuccess/isError แนวทางนี้มีประโยชน์โดยเฉพาะใน Clean Architecture ที่แต่ละเลเยอร์ (data, domain, presentation) สามารถแปลงข้อผิดพลาด: IOError -> DomainError -> UiError การ propagation ผ่านคอนเทนเนอร์ทำให้การแปลงเหล่านี้ชัดเจนและทดสอบได้ ตรงข้ามกับข้อยกเว้นที่ห่วงโซ่การแปลงไม่ปรากฏในลายเซ็นต์ฟังก์ชัน
หนึ่งในการตัดสินใจสำคัญในการออกแบบการจัดการข้อผิดพลาดคือการเลือกระหว่าง propagation (ส่งขึ้นไป) และ handling (จัดการที่นี่) กฎการตัดสินใจ: จัดการข้อผิดพลาดในระดับที่มีบริบทเพียงพอสำหรับการกระทำที่มีความหมาย ถ้าคุณสามารถเข้าถึง UI — แสดงข้อความให้ผู้ใช้ ถ้าคุณสามารถเข้าถึงแคช — พยายามกู้คืน ถ้าคุณไม่มีทั้งสองอย่าง — propagate
| สถานการณ์ | การกระทำ | เหตุผล |
|---|---|---|
| ข้อผิดพลาดเครือข่ายใน Repository | Propagate | Repository ไม่รู้ว่าผู้ใช้ต้องการลองอีกครั้งหรือไม่ |
| ข้อผิดพลาดการแยกวิเคราะห์ใน Repository | จัดการ (ส่งค่าเริ่มต้น) | Repository รู้รูปแบบ สามารถส่งค่าสำรองได้ |
| หมดเวลาใน ViewModel | จัดการ (UiState.Error) | ViewModel จัดการ UiState รู้วิธีแปลข้อผิดพลาด |
| ข้อผิดพลาดการอนุญาตใน Interceptor | จัดการ (รีเฟรชโทเค็น) | Interceptor สามารถเข้าถึงโทเค็นและกู้คืนเซสชันได้ |
| ข้อผิดพลาดที่ไม่รู้จักใน UseCase | Propagate | UseCase ไม่มีบริบท UI — มีเพียงตรรกะทางธุรกิจ |
กฎทอง: propagation น้อยที่สุด การจัดการมากที่สุดในระดับล่าง ถ้า Repository สามารถกู้คืนจากแคชได้ — มันควรทำโดยไม่ส่งข้อผิดพลาดขึ้นไป ถ้า ViewModel สามารถแสดง Snackbar ได้ — ให้มันแสดง โดยไม่ต้องขอโค้ดเพิ่มเติมจาก ViewController แต่ละระดับของ propagation เพิ่มการเชื่อมโยงและทำให้การทดสอบซับซ้อนขึ้น ตาม Google Android Architecture Guide (2026) แนะนำให้ลด propagation ข้ามขอบเขตเลเยอร์โดยใช้ sealed class UiState เพื่อแสดงสถานะที่เป็นไปได้ทั้งหมด (Loading, Success, Error) ที่ระดับ ViewModel และไม่ส่งข้อยกเว้นโดยตรงไปยังเลเยอร์ UI
การ propagation ที่ไม่ถูกต้องเป็นแหล่งของบั๊กที่ยากต่อการค้นหาในแอปพลิเคชันมือถือ ลองพิจารณาปัญหาหลักห้าประการที่นักพัฒนาพบและวิธีการแก้ไข
ปัญหาที่พบบ่อยที่สุด: ระหว่างการ propagation ข้อยกเว้นถูกจับ ถูกบันทึก และมีการโยนใหม่โดยไม่มีข้อยกเว้นดั้งเดิม นักพัฒนาสูญเสีย 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+ ระดับโดยไม่ถูกจัดการ สถาปัตยกรรมต้องได้รับการพิจารณาใหม่ แต่ละระดับของ propagation คือการพึ่งพาลายเซ็นต์ throws ของฟังก์ชันที่อยู่ด้านล่าง วิธีแก้: ใช้ คอนเทนเนอร์ความล้มเหลว (sealed class Result { Success, Error }) ที่ขอบเขตเลเยอร์เพื่อทำให้ propagation ชัดเจนและจำกัด ยิ่งห่วงโซ่ propagation สั้นเท่าไหร่ การทดสอบและการดีบักโค้ดก็ง่ายขึ้นเท่านั้น
ใน Kotlin Coroutines ข้อยกเว้นใน launch โดยค่าเริ่มต้นจะยกเลิกคอรูนแม่และ siblings ทั้งหมด (ลูกของขอบเขตเดียวกัน) ถ้าหนึ่งใน 10 งานที่ทำงานพร้อมกันล้มเหลว อีก 9 งานจะถูกยกเลิก ซึ่งมักไม่เป็นที่ต้องการ ใช้ SupervisorJob หรือ supervisorScope เพื่อแยกข้อผิดพลาด: ข้อผิดพลาดในลูกหนึ่งตัวไม่ยกเลิก siblings ViewModelScope ใช้ SupervisorJob โดยค่าเริ่มต้น ซึ่งป้องกันปัญหานี้ใน Android
ใน API ที่ใช้ callback เป็นฐาน ข้อผิดพลาดมักถูกส่งเป็นพารามิเตอร์ของ callback ถ้า callback ไม่จัดการข้อผิดพลาด (หรือจัดการไม่ถูกต้อง) propagation กลายเป็นโดยนัยและสูญหายได้ง่าย วิธีแก้: ย้ายไปใช้ async/await (Swift) หรือคอรูน (Kotlin) ที่ propagation ทำงานผ่านกลไก try-catch มาตรฐาน ถ้า callback หลีกเลี่ยงไม่ได้ — ใช้ Either<Error, T> หรือ Result<T> เพื่อบังคับจัดการทั้งสองกรณี
คำถามที่พบบ่อย
Throw คือการกระทำครั้งเดียวในการโยนข้อยกเว้น Error Propagation คือกระบวนการทั้งหมดของการส่งข้อผิดพลาดผ่านหลายระดับสแต็ก ตั้งแต่ throw ถึง catch Propagation รวมถึง throw การส่งต่ออัตโนมัติหรือด้วยตนเองผ่านฟังก์ชันกลาง และการจัดการสุดท้าย มันเป็นแนวคิดที่กว้างกว่าที่อธิบายวงจรชีวิตของข้อผิดพลาด
ใช้ ออบเจ็กต์จำลอง ที่โยนข้อยกเว้นในสถานการณ์ที่กำหนด ตรวจสอบว่าฟังก์ชัน propagate หรือจัดการข้อผิดพลาดอย่างถูกต้องผ่าน assertThrows (Kotlin/JUnit) หรือ XCTAssertThrowsError (Swift/XCTest) สำหรับ propagation ที่ใช้ Result เป็นฐาน ตรวจสอบ isSuccess/isError และค่าในทั้งสองกรณี
Propagation ผ่าน Result เหมาะสำหรับข้อผิดพลาดที่คาดหวัง (ข้อมูลไม่ถูกต้อง กฎทางธุรกิจ) ภายในขอบเขตสถาปัตยกรรมเดียว ข้อยกเว้นดีกว่าสำหรับข้อผิดพลาดที่ไม่คาดคิด (การสูญเสียเครือข่าย ข้อผิดพลาด I/O) ที่ควรถูกจัดการในระดับสูง ผลลัพธ์ที่มีข้อผิดพลาดไม่ขัดจังหวะการไหลของการดำเนินการ ข้อยกเว้นขัดจังหวะ
ใน Kotlin Coroutines ข้อยกเว้นใน launch แพร่กระจายโดยอัตโนมัติผ่าน CoroutineScope พร้อมการยกเลิก siblings ใช้ supervisorScope หรือ SupervisorJob เพื่อแยก: ข้อผิดพลาดในคอรูนหนึ่งไม่ยกเลิกคอรูนอื่น สำหรับ async ต้องจัดการข้อผิดพลาดอย่างชัดเจนผ่าน try-catch เมื่อเรียก await() มิฉะนั้นมันจะถูกกลืน
ตัวจัดการสุดท้ายในอุดมคติคือ เลเยอร์ UI (ViewController, Fragment/Composable) เพียงมันเท่านั้นที่สามารถเข้าถึงอินเทอร์เฟซผู้ใช้และสามารถแสดงข้อความ Snackbar หรือไดอะล็อกได้ เลเยอร์กลาง (Repository, UseCase, ViewModel) propagate ข้อผิดพลาด แปลงมันหากจำเป็นเป็นประเภทโดเมนที่เป็นนามธรรมมากขึ้น
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม