Error Propagation: คืออะไร กลไกการแพร่กระจายข้อผิดพลาด และทำงานอย่างไรในการพัฒนาแอปมือถือ

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-05-26 เวลาอ่าน: 9 นาที

Error Propagation เป็นกลไกในการแพร่กระจายข้อผิดพลาดขึ้นไปตามสแต็กการเรียกจากจุดที่เกิดขึ้นไปยังตัวจัดการ เมื่อฟังก์ชันไม่สามารถจัดการข้อผิดพลาดได้ด้วยตัวเอง มันจะส่งต่อข้อผิดพลาดไปยังฝ่ายที่เรียกผ่านข้อยกเว้น (exception) การประกาศ throws หรือ Return Type การใช้งาน propagation ที่ถูกต้องเป็นสิ่งสำคัญสำหรับความเสถียรของแอปพลิเคชันมือถือ: ข้อผิดพลาดที่ไม่ถูกจัดการหรือส่งต่ออย่างไม่ถูกต้องนำไปสู่การขัดข้อง ตาม Apple Swift Documentation (2026) propagation อัตโนมัติ ผ่าน throws ใน Swift ช่วยให้ส่งข้อผิดพลาดไปยังระดับใดก็ได้โดยไม่ต้องใช้โค้ดที่ซ้ำซ้อน

ประเด็นสำคัญ

  • Error Propagation — การส่งข้อผิดพลาดจากจุดที่เกิดขึ้นขึ้นไปตามสแต็กไปยังตัวจัดการ โดยข้ามฟังก์ชันกลาง
  • propagation อัตโนมัติ ผ่าน throws ใน Swift ส่งข้อผิดพลาดโดยไม่ต้องใช้โค้ดที่ชัดเจนในแต่ละระดับสแต็ก
  • propagation ด้วยตนเอง ใน Kotlin และ Dart ต้องใช้ try-catch ที่ชัดเจนหรือการส่งในคอนเทนเนอร์ Result ในแต่ละระดับ
  • Checked exceptions ใน Java บังคับให้ propagation ผ่าน throws ในลายเซ็นต์ unchecked สามารถละเว้นได้
  • Result Type — ทางเลือกแทนข้อยกเว้นที่ข้อผิดพลาดถูกส่งเป็นค่าโดยไม่ต้องคลายสแต็ก

Error Propagation คืออะไร?

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 ให้เหลือน้อยที่สุด จัดการข้อผิดพลาดในระดับต่ำสุดที่เป็นไปได้ซึ่งมีบริบทเพียงพอสำหรับการตัดสินใจ

Propagation ผ่าน Throws ใน Swift

ใน Swift การ propagation ผ่าน throws เป็นอัตโนมัติ: ถ้าฟังก์ชัน A ที่มี throws เรียกฟังก์ชัน B ที่มี throws และ A ไม่จัดการข้อผิดพลาดของ B ใน do-catch ข้อผิดพลาดจะถูกส่งไปยังผู้เรียกของ A โดยอัตโนมัติ สิ่งนี้กำจัดโค้ดที่ซ้ำซ้อนทั่วไปของ 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("ไม่สามารถโหลดผู้ใช้ได้")
    }
}

ห่วงโซ่ propagation: networkService.request -> fetchUser -> loadUser -> onButtonTap ฟังก์ชันกลางแต่ละตัวถูกทำเครื่องหมาย throws และไม่มี do-catch — ข้อผิดพลาดถูกส่งขึ้นไปโดยอัตโนมัติ ViewController onButtonTap เป็นตัวจัดการสุดท้ายที่มี do-catch ถ้า ViewModel ตัดสินใจแปลงข้อผิดพลาด (ห่อเป็นประเภทอื่น) มันสามารถใช้ do-catch และ throw ใหม่ได้ propagation อัตโนมัติ ลดโค้ด: Repository ไม่จำเป็นต้องรู้วิธีจัดการข้อผิดพลาด — นั่นคือความรับผิดชอบของ ViewController ที่สามารถเข้าถึง UI เพื่อแสดงข้อความให้ผู้ใช้

Propagation ผ่านข้อยกเว้นใน Kotlin

ใน Kotlin การ propagation ผ่านข้อยกเว้นไม่ต้องการการประกาศ throws ในลายเซ็นต์ (ข้อยกเว้นทั้งหมดเป็น unchecked) ข้อยกเว้นจะถูกยกขึ้นไปตามสแต็กโดยอัตโนมัติจนกว่าจะพบ try-catch อย่างไรก็ตาม การไม่มี throws ในลายเซ็นต์ทำให้ propagation เป็นโดยนัย: นักพัฒนาไม่สามารถเห็นจากลายเซ็นต์ฟังก์ชันว่ามันอาจโยนข้อยกเว้น นี่เป็นทั้งข้อดี (โค้ดซ้ำซ้อนน้อยกว่า) และข้อเสีย (ลืมจัดการได้ง่ายกว่า) ในเวลาเดียวกัน 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 propagation ด้วยการแปลง: เมื่อ IOException (เครือข่ายไม่พร้อมใช้งาน) ฟังก์ชันพยายามรับข้อมูลที่ถูกแคชจากฐานข้อมูล ถ้าแคชว่าง มันจะโยน AppException — propagation ดำเนินต่อด้วยประเภทข้อผิดพลาดใหม่ ViewModel จับ AppException และแปลงเป็น UiState.Error — ข้อผิดพลาดไม่เดินหน้าต่อไป propagation สิ้นสุดที่ระดับเลเยอร์ UI Kotlin Coroutines เพิ่มลักษณะเฉพาะ: ข้อยกเว้นใน launch แพร่กระจายโดยอัตโนมัติผ่าน CoroutineExceptionHandler และใน async — เฉพาะเมื่อเรียก await() สิ่งนี้สำคัญเมื่อออกแบบ propagation ในคอรูน — SupervisorJob ป้องกันการยกเลิกคอรูนแม่เมื่อเกิดข้อผิดพลาดในคอรูนลูก

Propagation ผ่าน Result Type

ทางเลือกแทนข้อยกเว้นคือการ propagation ผ่านประเภทคอนเทนเนอร์ที่ส่งความสำเร็จหรือข้อผิดพลาดเป็นค่า ในแนวทางนี้ ฟังก์ชันไม่ส่งค่ากลับแต่ส่ง wrapper: Result<T, E> ใน Swift, Result<T> ใน Kotlin, Either<L, R> ใน Dart (จากแพ็กเกจ fpdart หรือ dartz) ข้อผิดพลาดไม่คลายสแต็ก — มันแค่อยู่ในคอนเทนเนอร์ และระดับถัดไปตัดสินใจว่าจะทำอย่างไรกับมัน สิ่งนี้ทำให้ propagation ชัดเจนและควบคุมได้มากขึ้น

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, propagation ไม่ต้องการการคลายสแต็ก — ผู้เรียกเพียงแค่ตรวจสอบ isSuccess/isError แนวทางนี้มีประโยชน์โดยเฉพาะใน Clean Architecture ที่แต่ละเลเยอร์ (data, domain, presentation) สามารถแปลงข้อผิดพลาด: IOError -> DomainError -> UiError การ propagation ผ่านคอนเทนเนอร์ทำให้การแปลงเหล่านี้ชัดเจนและทดสอบได้ ตรงข้ามกับข้อยกเว้นที่ห่วงโซ่การแปลงไม่ปรากฏในลายเซ็นต์ฟังก์ชัน

Propagation กับ Handling: เมื่อใดควรส่งต่อ เมื่อใดควรจัดการ

หนึ่งในการตัดสินใจสำคัญในการออกแบบการจัดการข้อผิดพลาดคือการเลือกระหว่าง propagation (ส่งขึ้นไป) และ handling (จัดการที่นี่) กฎการตัดสินใจ: จัดการข้อผิดพลาดในระดับที่มีบริบทเพียงพอสำหรับการกระทำที่มีความหมาย ถ้าคุณสามารถเข้าถึง UI — แสดงข้อความให้ผู้ใช้ ถ้าคุณสามารถเข้าถึงแคช — พยายามกู้คืน ถ้าคุณไม่มีทั้งสองอย่าง — propagate

สถานการณ์การกระทำเหตุผล
ข้อผิดพลาดเครือข่ายใน RepositoryPropagateRepository ไม่รู้ว่าผู้ใช้ต้องการลองอีกครั้งหรือไม่
ข้อผิดพลาดการแยกวิเคราะห์ใน Repositoryจัดการ (ส่งค่าเริ่มต้น)Repository รู้รูปแบบ สามารถส่งค่าสำรองได้
หมดเวลาใน ViewModelจัดการ (UiState.Error)ViewModel จัดการ UiState รู้วิธีแปลข้อผิดพลาด
ข้อผิดพลาดการอนุญาตใน Interceptorจัดการ (รีเฟรชโทเค็น)Interceptor สามารถเข้าถึงโทเค็นและกู้คืนเซสชันได้
ข้อผิดพลาดที่ไม่รู้จักใน UseCasePropagateUseCase ไม่มีบริบท UI — มีเพียงตรรกะทางธุรกิจ

กฎทอง: propagation น้อยที่สุด การจัดการมากที่สุดในระดับล่าง ถ้า Repository สามารถกู้คืนจากแคชได้ — มันควรทำโดยไม่ส่งข้อผิดพลาดขึ้นไป ถ้า ViewModel สามารถแสดง Snackbar ได้ — ให้มันแสดง โดยไม่ต้องขอโค้ดเพิ่มเติมจาก ViewController แต่ละระดับของ propagation เพิ่มการเชื่อมโยงและทำให้การทดสอบซับซ้อนขึ้น ตาม Google Android Architecture Guide (2026) แนะนำให้ลด propagation ข้ามขอบเขตเลเยอร์โดยใช้ sealed class UiState เพื่อแสดงสถานะที่เป็นไปได้ทั้งหมด (Loading, Success, Error) ที่ระดับ ViewModel และไม่ส่งข้อยกเว้นโดยตรงไปยังเลเยอร์ UI

ปัญหาและรูปแบบต้านของ Error Propagation

การ propagation ที่ไม่ถูกต้องเป็นแหล่งของบั๊กที่ยากต่อการค้นหาในแอปพลิเคชันมือถือ ลองพิจารณาปัญหาหลักห้าประการที่นักพัฒนาพบและวิธีการแก้ไข

การสูญเสียบริบทข้อผิดพลาด

ปัญหาที่พบบ่อยที่สุด: ระหว่างการ 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 { /* บันทึก */ } อย่ากลืนข้อยกเว้นโดยไม่บันทึก

ความลึกของ propagation มากเกินไป

ถ้าข้อผิดพลาดผ่าน 5+ ระดับโดยไม่ถูกจัดการ สถาปัตยกรรมต้องได้รับการพิจารณาใหม่ แต่ละระดับของ propagation คือการพึ่งพาลายเซ็นต์ throws ของฟังก์ชันที่อยู่ด้านล่าง วิธีแก้: ใช้ คอนเทนเนอร์ความล้มเหลว (sealed class Result { Success, Error }) ที่ขอบเขตเลเยอร์เพื่อทำให้ propagation ชัดเจนและจำกัด ยิ่งห่วงโซ่ propagation สั้นเท่าไหร่ การทดสอบและการดีบักโค้ดก็ง่ายขึ้นเท่านั้น

Propagation ในคอรูนโดยไม่มี SupervisorJob

ใน Kotlin Coroutines ข้อยกเว้นใน launch โดยค่าเริ่มต้นจะยกเลิกคอรูนแม่และ siblings ทั้งหมด (ลูกของขอบเขตเดียวกัน) ถ้าหนึ่งใน 10 งานที่ทำงานพร้อมกันล้มเหลว อีก 9 งานจะถูกยกเลิก ซึ่งมักไม่เป็นที่ต้องการ ใช้ SupervisorJob หรือ supervisorScope เพื่อแยกข้อผิดพลาด: ข้อผิดพลาดในลูกหนึ่งตัวไม่ยกเลิก siblings ViewModelScope ใช้ SupervisorJob โดยค่าเริ่มต้น ซึ่งป้องกันปัญหานี้ใน Android

Propagation ผ่าน callback โดยไม่จัดการ

ใน API ที่ใช้ callback เป็นฐาน ข้อผิดพลาดมักถูกส่งเป็นพารามิเตอร์ของ callback ถ้า callback ไม่จัดการข้อผิดพลาด (หรือจัดการไม่ถูกต้อง) propagation กลายเป็นโดยนัยและสูญหายได้ง่าย วิธีแก้: ย้ายไปใช้ async/await (Swift) หรือคอรูน (Kotlin) ที่ propagation ทำงานผ่านกลไก try-catch มาตรฐาน ถ้า callback หลีกเลี่ยงไม่ได้ — ใช้ Either<Error, T> หรือ Result<T> เพื่อบังคับจัดการทั้งสองกรณี

คำถามที่พบบ่อย

Error Propagation แตกต่างจาก throw อย่างไร?

Throw คือการกระทำครั้งเดียวในการโยนข้อยกเว้น Error Propagation คือกระบวนการทั้งหมดของการส่งข้อผิดพลาดผ่านหลายระดับสแต็ก ตั้งแต่ throw ถึง catch Propagation รวมถึง throw การส่งต่ออัตโนมัติหรือด้วยตนเองผ่านฟังก์ชันกลาง และการจัดการสุดท้าย มันเป็นแนวคิดที่กว้างกว่าที่อธิบายวงจรชีวิตของข้อผิดพลาด

จะทดสอบ Error Propagation ได้อย่างไร?

ใช้ ออบเจ็กต์จำลอง ที่โยนข้อยกเว้นในสถานการณ์ที่กำหนด ตรวจสอบว่าฟังก์ชัน propagate หรือจัดการข้อผิดพลาดอย่างถูกต้องผ่าน assertThrows (Kotlin/JUnit) หรือ XCTAssertThrowsError (Swift/XCTest) สำหรับ propagation ที่ใช้ Result เป็นฐาน ตรวจสอบ isSuccess/isError และค่าในทั้งสองกรณี

เมื่อใดการ propagation ผ่าน Result ดีกว่าข้อยกเว้น?

Propagation ผ่าน Result เหมาะสำหรับข้อผิดพลาดที่คาดหวัง (ข้อมูลไม่ถูกต้อง กฎทางธุรกิจ) ภายในขอบเขตสถาปัตยกรรมเดียว ข้อยกเว้นดีกว่าสำหรับข้อผิดพลาดที่ไม่คาดคิด (การสูญเสียเครือข่าย ข้อผิดพลาด I/O) ที่ควรถูกจัดการในระดับสูง ผลลัพธ์ที่มีข้อผิดพลาดไม่ขัดจังหวะการไหลของการดำเนินการ ข้อยกเว้นขัดจังหวะ

จะแพร่กระจายข้อผิดพลาดผ่านคอรูน Kotlin ได้อย่างไร?

ใน Kotlin Coroutines ข้อยกเว้นใน launch แพร่กระจายโดยอัตโนมัติผ่าน CoroutineScope พร้อมการยกเลิก siblings ใช้ supervisorScope หรือ SupervisorJob เพื่อแยก: ข้อผิดพลาดในคอรูนหนึ่งไม่ยกเลิกคอรูนอื่น สำหรับ async ต้องจัดการข้อผิดพลาดอย่างชัดเจนผ่าน try-catch เมื่อเรียก await() มิฉะนั้นมันจะถูกกลืน

ระดับใดควรเป็นตัวจัดการข้อผิดพลาดสุดท้าย?

ตัวจัดการสุดท้ายในอุดมคติคือ เลเยอร์ UI (ViewController, Fragment/Composable) เพียงมันเท่านั้นที่สามารถเข้าถึงอินเทอร์เฟซผู้ใช้และสามารถแสดงข้อความ Snackbar หรือไดอะล็อกได้ เลเยอร์กลาง (Repository, UseCase, ViewModel) propagate ข้อผิดพลาด แปลงมันหากจำเป็นเป็นประเภทโดเมนที่เป็นนามธรรมมากขึ้น

สรุป

  • Error Propagation — การส่งข้อผิดพลาดขึ้นไปตามสแต็กจากจุดที่เกิดขึ้นไปยังตัวจัดการผ่านข้อยกเว้นหรือคอนเทนเนอร์ Result
  • Propagation อัตโนมัติ ใน Swift ผ่าน throws ไม่ต้องใช้โค้ดในระดับกลาง — ข้อผิดพลาดขึ้นไปเอง
  • Propagation ด้วยตนเอง ใน Kotlin ผ่าน try-catch ที่ชัดเจนและ throw ในแต่ละระดับทำให้การจัดการมีสติแต่冗长
  • Result Type ส่งข้อผิดพลาดเป็นค่าโดยไม่คลายสแต็ก สะดวกสำหรับข้อผิดพลาดที่คาดหวังใน Clean Architecture
  • กฎการจัดการ: จัดการในระดับที่มีบริบท (UI) propagate ผ่านระดับที่ไม่มีบริบท (domain, data)
  • รูปแบบต้าน: catch ว่าง การสูญเสีย cause ในการแปลง ความลึก propagation มากเกินไป การละเว้น SupervisorJob
  • ออกแบบข้อผิดพลาดที่ระบุประเภท (sealed class / enum) สำหรับแต่ละเลเยอร์และแปลงเมื่อข้ามขอบเขตเลเยอร์

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม