Διάδοση Σφαλμάτων — μηχανισμός διάδοσης του σφάλματος προς τα πάνω στη στοίβα κλήσεων από το σημείο εμφάνισης έως τον χειριστή. Όταν μια συνάρτηση δεν μπορεί να χειριστεί το σφάλμα ανεξάρτητα, το μεταδίδει στην καλούσα πλευρά μέσω εξαίρεσης (exception), δήλωσης throws ή τύπου επιστροφής. Η σωστή υλοποίηση της διάδοσης είναι κρίσιμη για τη σταθερότητα των κινητών εφαρμογών: τα μη επεξεργασμένα ή εσφαλμένα μεταδιδόμενα σφάλματα οδηγούν σε καταρρεύσεις. Σύμφωνα με το Apple Swift Documentation (2026), η αυτόματη διάδοση μέσω throws στη Swift επιτρέπει τη μετάδοση του σφάλματος σε οποιοδήποτε επίπεδο χωρίς boilerplate κώδικα.
Κύρια Σημεία
Διάδοση Σφαλμάτων — η διαδικασία μετάδοσης του αντικειμένου σφάλματος από τη συνάρτηση όπου προέκυψε προς τα πάνω στην αλυσίδα κλήσεων μέχρι τον πλησιέστερο κατάλληλο χειριστή. Φανταστείτε τη στοίβα κλήσεων: 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 (για αναμενόμενα σφάλματα). Είναι σημαντικό να κατανοήσετε: η διάδοση δεν είναι στόχος, αλλά αναγκαιότητα. Η ιδανική αρχιτεκτονική ελαχιστοποιεί το βάθος διάδοσης, χειριζόμενη τα σφάλματα στο χαμηλότερο δυνατό επίπεδο όπου υπάρχει αρκετό περιεχόμενο για τη λήψη απόφασης.
Στη Swift, η διάδοση μέσω throws γίνεται αυτόματα: εάν η συνάρτηση A με throws καλεί τη συνάρτηση B με throws, και η A δεν χειρίζεται το σφάλμα της B σε do-catch, το σφάλμα μεταδίδεται αυτόματα στην καλούσα πλευρά της A. Αυτό εξαλείφει τον boilerplate κώδικα που χαρακτηρίζει τις ελεγμένες εξαιρέσεις Java, όπου το 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 | Χειρίσου (ανανέωσε το 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 (e: Exception) { /* τίποτα */ } — αντι-πρότυπο που οδηγεί στη συνέχιση λειτουργίας της εφαρμογής σε εσφαλμένη κατάσταση. Εάν είστε σίγουροι ότι το σφάλμα μπορεί να αγνοηθεί — προσθέστε ένα σχόλιο με αιτιολόγηση. Στη Swift για προαιρετική αγνόηση χρησιμοποιήστε try? (σφάλμα -> nil). Σε Kotlin — Result<T>.onFailure { /* log */ }. Μην καταπνίγετε εξαιρέσεις χωρίς καταγραφή.
Εάν το σφάλμα περνά από 5+ επίπεδα χωρίς χειρισμό, η αρχιτεκτονική απαιτεί επανεξέταση. Κάθε επίπεδο διάδοσης είναι μια εξάρτηση από την υπογραφή throws των υποκείμενων συναρτήσεων. Λύση: χρησιμοποιήστε κοντέινερ Failure (sealed class Result { Success, Error }) στα όρια των στρωμάτων για να κάνετε τη διάδοση ρητή και περιορισμένη. Όσο πιο σύντομη είναι η αλυσίδα διάδοσης, τόσο πιο εύκολο είναι να δοκιμάσετε και να διορθώσετε τον κώδικα.
Σε Kotlin Coroutines, η εξαίρεση στο launch από προεπιλογή ακυρώνει τη γονική κορουτίνα και όλα τα sibling (παιδιά του ίδιου scope). Εάν μία από 10 παράλληλες εργασίες αποτύχει, οι υπόλοιπες 9 θα ακυρωθούν, κάτι που συχνά είναι ανεπιθύμητο. Χρησιμοποιήστε SupervisorJob ή supervisorScope για απομόνωση σφαλμάτων: ένα σφάλμα σε ένα child δεν ακυρώνει τα sibling. Το 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 προτιμάται για αναμενόμενα σφάλματα (μη έγκυρα δεδομένα, επιχειρηματικοί κανόνες) εντός ενός αρχιτεκτονικού ορίου. Οι εξαιρέσεις είναι καλύτερες για απροσδόκητα σφάλματα (απώλεια δικτύου, σφάλματα I/O) που πρέπει να χειριστούν σε υψηλό επίπεδο. Το αποτέλεσμα με σφάλμα δεν διακόπτει τη ροή εκτέλεσης, η εξαίρεση τη διακόπτει.
Σε Kotlin Coroutines, η εξαίρεση στο launch διαδίδεται αυτόματα μέσω CoroutineScope με ακύρωση των sibling. Χρησιμοποιήστε supervisorScope ή SupervisorJob για απομόνωση: ένα σφάλμα σε μια κορουτίνα δεν ακυρώνει τις άλλες. Για async, το σφάλμα πρέπει να χειριστεί ρητά μέσω try-catch κατά την κλήση await(), διαφορετικά θα καταποθεί.
Ο ιδανικός τελικός χειριστής είναι το στρώμα UI (ViewController, Fragment/Composable). Μόνο αυτό έχει πρόσβαση στη διεπαφή χρήστη και μπορεί να εμφανίσει μήνυμα, Snackbar ή διάλογο. Τα ενδιάμεσα στρώματα (Repository, UseCase, ViewModel) διαδίδουν το σφάλμα, μετασχηματίζοντάς το εάν χρειάζεται σε πιο αφηρημένο τύπο τομέα.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης