Διάδοση Σφαλμάτων: τι είναι, μηχανισμός διάδοσης σφαλμάτων και πώς λειτουργεί στην κινητή ανάπτυξη

Συγγραφέας: 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 ελεγμένες εξαιρέσεις) ο μεταγλωττιστής εξαναγκάζει τον προγραμματιστή είτε να χειριστεί το σφάλμα είτε να δηλώσει τη διάδοση στην υπογραφή. Στη χειροκίνητη προσέγγιση (τύπος 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, όπου το 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. Η διάδοση μέσω κοντέινερ καθιστά αυτούς τους μετασχηματισμούς ρητούς και δοκιμάσιμους, σε αντίθεση με τις εξαιρέσεις όπου η αλυσίδα μετασχηματισμών δεν είναι ορατή στις υπογραφές των συναρτήσεων.

Διάδοση vs Χειρισμός: πότε να μεταδίδετε, πότε να χειρίζεστε

Μία από τις βασικές αποφάσεις στον σχεδιασμό χειρισμού σφαλμάτων είναι η επιλογή μεταξύ διάδοσης (μετάδοση προς τα πάνω) και χειρισμού (χειρισμός εδώ). Ο κανόνας λήψης απόφασης: χειριστείτε το σφάλμα στο επίπεδο όπου υπάρχει αρκετό περιεχόμενο για ουσιαστική ενέργεια. Εάν έχετε πρόσβαση στο UI — εμφανίστε ένα μήνυμα στον χρήστη. Εάν έχετε πρόσβαση στην προσωρινή μνήμη — προσπαθήστε να ανακάμψετε. Εάν δεν έχετε κανένα από τα δύο — διαδώστε.

ΣενάριοΕνέργειαΑιτιολόγηση
Σφάλμα δικτύου στο RepositoryΔιάδωσεΤο Repository δεν γνωρίζει εάν ο χρήστης θέλει να επαναλάβει το αίτημα
Σφάλμα ανάλυσης στο RepositoryΧειρίσου (επέστρεψε προεπιλεγμένη τιμή)Το Repository γνωρίζει τη μορφή, μπορεί να επιστρέψει εναλλακτική τιμή
Λήξη χρόνου στο ViewModelΧειρίσου (UiState.Error)Το ViewModel διαχειρίζεται το UiState, γνωρίζει πώς να μεταφράσει το σφάλμα
Σφάλμα εξουσιοδότησης στο InterceptorΧειρίσου (ανανέωσε το token)Το Interceptor έχει πρόσβαση στα tokens και μπορεί να αποκαταστήσει τη συνεδρία
Άγνωστο σφάλμα στο 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 από προεπιλογή ακυρώνει τη γονική κορουτίνα και όλα τα sibling (παιδιά του ίδιου scope). Εάν μία από 10 παράλληλες εργασίες αποτύχει, οι υπόλοιπες 9 θα ακυρωθούν, κάτι που συχνά είναι ανεπιθύμητο. Χρησιμοποιήστε SupervisorJob ή supervisorScope για απομόνωση σφαλμάτων: ένα σφάλμα σε ένα child δεν ακυρώνει τα sibling. Το 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 προτιμάται για αναμενόμενα σφάλματα (μη έγκυρα δεδομένα, επιχειρηματικοί κανόνες) εντός ενός αρχιτεκτονικού ορίου. Οι εξαιρέσεις είναι καλύτερες για απροσδόκητα σφάλματα (απώλεια δικτύου, σφάλματα I/O) που πρέπει να χειριστούν σε υψηλό επίπεδο. Το αποτέλεσμα με σφάλμα δεν διακόπτει τη ροή εκτέλεσης, η εξαίρεση τη διακόπτει.

Πώς να διαδώσετε σφάλμα μέσω κορουτινών Kotlin;

Σε Kotlin Coroutines, η εξαίρεση στο launch διαδίδεται αυτόματα μέσω CoroutineScope με ακύρωση των sibling. Χρησιμοποιήστε 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 δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης