Felspridning — mekanism för att sprida ett fel uppåt i anropsstacken från uppkomstplatsen till hanteraren. När en funktion inte kan hantera felet självständigt skickar hon det vidare till den anropande parten via ett undantag (exception), throws-deklaration eller returtyp. Korrekt implementering av spridning är avgörande för stabiliteten hos mobila applikationer: ohanterade eller felaktigt vidarebefordrade fel leder till krascher. Enligt Apple Swift Documentation (2026) möjliggör automatisk spridning genom throws i Swift att skicka felet till vilken nivå som helst utan boilerplate-kod.
Huvudpunkter
Felspridning — processen att överföra felobjektet från funktionen där det uppstod uppåt i anropskedjan till närmaste lämpliga hanterare. Föreställ dig anropsstacken: ViewController anropar ViewModel, ViewModel anropar Repository, Repository anropar API. Om API returnerar ett nätverksfel måste det passera genom Repository och ViewModel till ViewController, som kommer att visa ett meddelande för användaren. Varje mellanliggande funktion bestämmer: hantera felet eller skicka det vidare (sprida).
Det finns två tillvägagångssätt för spridning: automatisk och manuell. I det automatiska tillvägagångssättet (Swift throws, Java kontrollerade undantag) tvingar kompilatorn utvecklaren att antingen hantera felet eller deklarera spridning i signaturen. I det manuella tillvägagångssättet (Result-typ, Kotlin Try) överförs felet som ett värde — utvecklaren skriver explicit kod för överföring eller transformering av felet. Enligt Kotlin Result Docs (2026) är Result<T> i Kotlin inte avsett för direkt spridning över funktionsgränser — det måste transformeras eller hanteras på varje nivå, vilket gör spridningen mer medveten men också mer ordrik.
Valet av tillvägagångssätt beror på applikationens arkitektur och språket. I Swift dominerar automatisk spridning genom throws, i Kotlin — en blandning av undantag (för oväntade fel) och Result-liknande behållare (för förväntade fel). Det är viktigt att förstå: spridning är inte ett mål, utan en nödvändighet. Ideal arkitektur minimerar spridningsdjupet genom att hantera fel på lägsta möjliga nivå där det finns tillräckligt med sammanhang för att fatta ett beslut.
I Swift sker spridning genom throws automatiskt: om funktion A med throws anropar funktion B med throws, och A inte hanterar B:s fel i do-catch, skickas felet automatiskt till A:s anropande part. Detta eliminerar boilerplate-koden som är karakteristisk för Java kontrollerade undantag, där throws måste deklareras i varje metod i kedjan. Swift använder principen „en throws-funktion i kedjan = hela kedjan blir throws, om det inte hanteras på mellanliggande nivåer”.
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 — slutlig hanterare
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Det gick inte att ladda användaren")
}
}
Spridningskedja: networkService.request -> fetchUser -> loadUser -> onButtonTap. Varje mellanliggande funktion är markerad med throws och innehåller inte do-catch — felet skickas automatiskt uppåt. ViewController onButtonTap är den slutliga hanteraren med do-catch. Om ViewModel hade beslutat att transformera felet (paketera det i en annan typ) kunde den ha använt do-catch och en ny throw. Automatisk spridning förkortar koden: Repository behöver inte veta hur felet ska hanteras — det är ViewControllers ansvar, som har åtkomst till UI för att visa ett meddelande för användaren.
I Kotlin kräver spridning genom undantag ingen throws-deklaration i signaturen (alla undantag är okontrollerade). Undantaget stiger automatiskt i stacken tills det stöter på try-catch. Avsaknaden av throws i signaturen gör dock spridningen implicit: utvecklaren ser inte från funktionens signatur att den kan kasta ett undantag. Detta är både en fördel (mindre boilerplate) och en nackdel (lättare att glömma att hantera). Kotlin löser detta problem genom konventioner och arkitekturmönster, inte genom språket.
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")
}
}
}
}
I Repository spridning med transformering: vid IOException (nätverk inte tillgängligt) försöker funktionen hämta cachad data från databasen. Om cachen är tom kastar den AppException — spridningen fortsätter med en ny feltyp. ViewModel fångar AppException och översätter det till UiState.Error — felet går inte vidare, spridningen avslutas på UI-lagrets nivå. Kotlin Coroutines lägger till särdrag: undantag i launch sprids automatiskt genom CoroutineExceptionHandler, och i async — endast vid anrop av await(). Det är viktigt att ta hänsyn till detta vid design av spridning i korutiner — SupervisorJob förhindrar annullering av föräldrakorutinen vid ett fel i en barnkorutin.
Ett alternativ till undantag — spridning genom en typbehållare som överför framgång eller fel som ett värde. I detta tillvägagångssätt returnerar funktionen inte ett värde, utan ett omslag: Result<T, E> i Swift, Result<T> i Kotlin, Either<L, R> i Dart (från paketet fpdart eller dartz). Felet rullar inte upp stacken — det ligger helt enkelt i behållaren och nästa nivå bestämmer vad som ska göras med det. Detta gör spridningen mer explicit och kontrollerbar.
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> — en enkel behållare med fälten data och error. Sealed class AppError definierar feltyper (Network, Auth). Funktionen fetchUser returnerar HttpResult, spridning kräver ingen upprullning av stacken — den anropande parten kontrollerar helt enkelt isSuccess/isError. Detta tillvägagångssätt är särskilt användbart i Clean Architecture, där varje lager (data, domain, presentation) kan transformera felet: IOError -> DomainError -> UiError. Spridning genom en behållare gör dessa transformeringar explicita och testbara, till skillnad från undantag där transformeringskedjan inte är synlig i funktionssignaturer.
Ett av de viktigaste besluten vid design av felhantering är valet mellan spridning (skicka uppåt) och hantering (hantera här). Beslutsregeln: hantera felet på den nivå där det finns tillräckligt med sammanhang för en meningsfull åtgärd. Om du har tillgång till UI — visa ett meddelande för användaren. Om du har tillgång till cache — försök återställa. Om du har ingetdera — sprid.
| Scenario | Åtgärd | Motivering |
|---|---|---|
| Nätverksfel i Repository | Sprid | Repository vet inte om användaren vill upprepa begäran |
| Parsningsfel i Repository | Hantera (returnera standardvärde) | Repository känner formatet, kan returnera ett reservvärde |
| Timeout i ViewModel | Hantera (UiState.Error) | ViewModel hanterar UiState, vet hur felet ska översättas |
| Auktoriseringsfel i Interceptor | Hantera (uppdatera token) | Interceptor har tillgång till tokens och kan återställa sessionen |
| Okänt fel i UseCase | Sprid | UseCase har ingen UI-kontext — endast affärslogik |
Gyllene regeln: minimal spridning, maximal hantering på lägre nivåer. Om Repository kan återhämta sig från cache — bör det göra det, utan att skicka felet uppåt. Om ViewModel kan visa en Snackbar — låt den visa den, utan att kräva extra kod från ViewController. Varje spridningsnivå ökar kopplingen och försvårar testning. Enligt Google Android Architecture Guide (2026) rekommenderas att minimera spridning över lagergränser genom att använda sealed class UiState för att representera alla möjliga tillstånd (Loading, Success, Error) på ViewModel-nivå och inte skicka undantag direkt till UI-lagret.
Felaktig spridning är en källa till svårupptäckta buggar i mobila applikationer. Låt oss undersöka fem huvudproblem som utvecklare stöter på och sätt att lösa dem.
Det vanligaste problemet: under spridning fångas undantaget, loggas och ett nytt kastas utan det ursprungliga undantaget. Utvecklaren förlorar StackTrace och kan inte förstå var felet exakt uppstod. I Swift använd felkedjning: throw MyError(context: originalError). I Kotlin: throw AppException(cause = originalException). I Dart: throw AppException(message, originalException). Skapa aldrig ett nytt undantag utan att skicka orsaken/underliggande fel.
catch (e: Exception) { /* ingenting */ } — ett antimönster som leder till att applikationen fortsätter fungera i ett felaktigt tillstånd. Om du är säker på att felet kan ignoreras — lägg till en kommentar med motivering. I Swift för valfri ignorering använd try? (fel -> nil). I Kotlin — Result<T>.onFailure { /* log */ }. Tysta inte undantag utan loggning.
Om felet passerar 5+ nivåer utan hantering kräver arkitekturen omprövning. Varje spridningsnivå är ett beroende av throws-signaturen hos underliggande funktioner. Lösning: använd Failure-behållare (sealed class Result { Success, Error }) på lagergränser för att göra spridningen explicit och begränsad. Ju kortare spridningskedja, desto lättare att testa och felsöka koden.
I Kotlin Coroutines annullerar ett undantag i launch som standard föräldrakorutinen och alla syskon (barn till samma scope). Om en av 10 parallella uppgifter misslyckas kommer de återstående 9 att annulleras, vilket ofta är oönskat. Använd SupervisorJob eller supervisorScope för isolering av fel: ett fel i ett child annullerar inte syskon. ViewModelScope använder SupervisorJob som standard, vilket skyddar mot detta problem i Android.
I callback-baserade API skickas felet ofta som en parameter till callbacken. Om callbacken inte hanterar felet (eller hanterar det felaktigt) blir spridningen implicit och går lätt förlorad. Lösning: migrera till async/await (Swift) eller korutiner (Kotlin), där spridning fungerar genom standardmekanismer som try-catch. Om en callback är oundviklig — använd Either<Error, T> eller Result<T> för tvångshantering av båda fallen.
Vanliga frågor
Throw är en engångsåtgärd att kasta ett undantag. Felspridning är hela processen att skicka ett fel genom flera stacknivåer, från throw till catch. Spridning inkluderar throw, automatisk eller manuell överföring via mellanliggande funktioner och slutlig hantering. Det är ett bredare begrepp som beskriver felets livscykel.
Använd mock-objekt som kastar undantag i givna scenarier. Kontrollera att funktionen korrekt sprider eller hanterar felet genom assertThrows (Kotlin/JUnit) eller XCTAssertThrowsError (Swift/XCTest). För Result-baserad spridning, kontrollera isSuccess/isError och värden i båda fallen.
Result-spridning är att föredra för förväntade fel (ogiltig data, affärsregler) inom en arkitekturgräns. Undantag är bättre för oväntade fel (nätverksförlust, I/O-fel) som bör hanteras på hög nivå. Ett resultat med fel avbryter inte exekveringsflödet, ett undantag avbryter det.
I Kotlin Coroutines sprider sig ett undantag i launch automatiskt genom CoroutineScope med annullering av syskon. Använd supervisorScope eller SupervisorJob för isolering: ett fel i en korutin annullerar inte andra. För async måste felet hanteras explicit genom try-catch vid anrop av await(), annars sväljs det.
Den ideala slutliga hanteraren är UI-lagret (ViewController, Fragment/Composable). Endast det har tillgång till användargränssnittet och kan visa ett meddelande, Snackbar eller dialog. Mellanliggande lager (Repository, UseCase, ViewModel) sprider felet och transformerar det vid behov till en mer abstrakt domäntyp.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också