Networking with the Ktor Client
|
This content was generated with the assistance of AI and should be verified against the official Kotlin Multiplatform documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Almost every app talks to a server, and in Kotlin Multiplatform (KMP) the network layer is the most natural thing to
share: requests, JSON parsing, authentication and error handling are pure logic, while only the socket-level
transport differs between platforms. The Ktor client is JetBrains' multiplatform HTTP client. You write an
HttpClient once in commonMain, and each platform plugs in the native engine it prefers: OkHttp on Android,
NSURLSession on iOS, and so on. Retrofit, the usual choice on Android, is JVM-only and cannot live in common code;
see Android: Networking with Retrofit, OkHttp and Ktor for
the Android-only story and for the comparison with Retrofit.
This page targets the section baseline: Kotlin 2.4.20, Ktor 3.6.0, kotlinx.serialization 1.11.0 and
kotlinx-coroutines 1.11.0, with Android through the com.android.kotlin.multiplatform.library plugin (AGP 9.4).
Stability is stated wherever it is not Stable: the Ktor client plugins used here are Stable, the Js and Wasm engine
follow the Kotlin/Wasm and Kotlin/JS status (Wasm is Beta), and Ktor’s WebSocket and Server-Sent-Events clients
are Stable but still marked with opt-in annotations on some members. The examples slice the running example,
TrailMate Everywhere (package com.example.trailmate), introduced in
What Kotlin Multiplatform Is and When to Use It.
Kotlin coroutines are not re-taught; see
Coroutines, Flow and Concurrency in Shared Code.
Gradle setup for dependencies is covered in
Dependencies, klibs.io, cinterop and SwiftPM Import.
Why Ktor in Shared Code
Ktor is built from the ground up on Kotlin coroutines and Kotlin Multiplatform, so a request is a suspend call that
works unchanged on every target.
| Option | Where it runs | Multiplatform | Typical use |
|---|---|---|---|
Ktor client |
JVM, Android, Apple, Linux, Windows, JS, Wasm |
Yes, common API with swappable engines |
The default for shared networking in KMP |
Retrofit + OkHttp |
JVM and Android |
No (JVM only) |
Android-only apps; see the Android section |
The trade-off is where the boundary sits: if the shared module owns networking, one client enforces the same authentication, retry and error semantics on every platform, at the cost of iOS developers no longer seeing requests in Swift tooling (Ktor’s logging plugin compensates). See Architecture, Shared ViewModels and State.
Architecture: One Client, Many Engines
An HttpClient has two parts: the client core with its plugins (JSON, auth, retries), which is pure common Kotlin,
and an engine that performs the actual I/O. The core artifact ktor-client-core goes in commonMain; engine
artifacts go in platform source sets. When you call HttpClient() without an argument in common code, Ktor picks the
engine found on the classpath of that target, which is why one engine dependency per target is enough.
ContentNegotiation, Auth, Retry, Logging"] API --> CORE end CORE -->|"androidMain"| OK["OkHttp engine"] CORE -->|"iosMain / appleMain"| DA["Darwin engine
(NSURLSession)"] CORE -->|"jvmMain (desktop)"| CIO["CIO or Java engine"] CORE -->|"wasmJsMain / jsMain"| JS["Js engine
(fetch)"]
Engines per Platform
| Target | Engine artifact | Notes | Choose it when |
|---|---|---|---|
Android |
|
Uses OkHttp, with its interceptors, connection pool and HTTP/2 |
You already use OkHttp, or want its ecosystem (certificate pinning, interceptors) |
JVM (desktop) |
|
Uses the JDK |
Desktop or server on Java 11+ without extra libraries |
iOS, macOS, tvOS, watchOS |
|
Uses |
Always on Apple targets (the recommended engine) |
Kotlin/JS, Kotlin/Wasm |
|
Uses the browser |
Browser targets; subject to CORS |
On Apple targets prefer ktor-client-darwin, because it integrates with system-level behaviour such as VPN, proxy
settings and certificate trust that a pure-Kotlin engine would bypass. ktor-client-cio is the pure-Kotlin alternative on Android and JVM (no HTTP/2) and also runs on Linux and Windows
native targets, and ktor-client-curl is the alternative on Linux and Windows desktop.
Declaring the Dependencies
The API goes in commonMain and each engine in its own source set. With the Android-KMP library plugin the
Android source set is androidMain.
// sharedLogic/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary) // com.android.kotlin.multiplatform.library
alias(libs.plugins.kotlinSerialization) // org.jetbrains.kotlin.plugin.serialization
}
kotlin {
android { namespace = "com.example.trailmate.shared" }
jvm()
iosArm64()
iosSimulatorArm64()
wasmJs { browser() }
sourceSets {
commonMain.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.auth)
}
androidMain.dependencies { implementation(libs.ktor.client.okhttp) }
jvmMain.dependencies { implementation(libs.ktor.client.java) }
iosMain.dependencies { implementation(libs.ktor.client.darwin) }
wasmJsMain.dependencies { implementation(libs.ktor.client.js) }
commonTest.dependencies { implementation(libs.ktor.client.mock) }
}
}
All Ktor artifacts share one version (ktor = "3.6.0" in the version catalog), so engines and plugins never drift
apart. The iosMain source set is shared by iosArm64 and iosSimulatorArm64 through the default hierarchy
template; see Project Structure, Targets, Source Sets and the Hierarchy Template.
See Ktor — Create a cross-platform mobile application
and Ktor — Client engines.
Providing the Engine: Default Discovery or an expect/actual Factory
For most apps HttpClient { … } in common code is enough, because Ktor discovers the single engine on each
target. When one target needs engine-specific configuration (an OkHttp interceptor, a Darwin cellular setting), use
the expect/actual pattern from
expect/actual and Calling Platform APIs, or,
cleaner, an interface injected by Koin. Here the engine factory is expect/actual:
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/HttpEngineFactory.kt
package com.example.trailmate.data.remote
import io.ktor.client.engine.HttpClientEngine
expect fun httpClientEngine(): HttpClientEngine
// sharedLogic/src/androidMain/kotlin/com/example/trailmate/data/remote/HttpEngineFactory.android.kt
package com.example.trailmate.data.remote
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
import java.util.concurrent.TimeUnit
actual fun httpClientEngine(): HttpClientEngine = OkHttp.create {
config {
// OkHttp-only tuning that has no common equivalent
retryOnConnectionFailure(true)
pingInterval(30, TimeUnit.SECONDS)
}
}
// sharedLogic/src/iosMain/kotlin/com/example/trailmate/data/remote/HttpEngineFactory.ios.kt
package com.example.trailmate.data.remote
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.darwin.Darwin
actual fun httpClientEngine(): HttpClientEngine = Darwin.create {
configureRequest {
// NSMutableURLRequest tuning: allow cellular access
setAllowsCellularAccess(true)
}
}
The JVM and Wasm actuals are one-liners, Java.create() and Js.create(), in jvmMain and wasmJsMain.
Passing the engine as a parameter (HttpClient(engine) { … }) is also what makes tests easy: the test swaps in a
MockEngine. See Ktor — Client engines for every engine and its options.
Creating the HttpClient
An HttpClient is heavyweight: it owns a connection pool and threads. Create one per app and reuse it, closing it
only when the app shuts down. Creating a client per request is the most common performance bug.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/HttpClientFactory.kt
package com.example.trailmate.data.remote
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
object HttpClientFactory {
fun create(engine: HttpClientEngine): HttpClient = HttpClient(engine) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true; explicitNulls = false })
}
install(HttpTimeout) {
connectTimeoutMillis = 10_000
requestTimeoutMillis = 30_000
socketTimeoutMillis = 30_000
}
install(Logging) {
logger = Logger.SIMPLE
level = LogLevel.INFO
}
defaultRequest {
url("https://api.example.com/v1/")
contentType(ContentType.Application.Json)
}
expectSuccess = true
}
}
Each plugin is explained in the sections below. See Ktor — Create and configure a client.
JSON with ContentNegotiation and kotlinx.serialization
ContentNegotiation converts request bodies to JSON and response bodies back to Kotlin objects. It needs a
serializer; on KMP that is ktor-serialization-kotlinx-json, which depends on kotlinx.serialization and works on
every target (Gson and Jackson do not, because they are JVM-only). The model classes carry @Serializable, which is
generated by the compiler plugin org.jetbrains.kotlin.plugin.serialization.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/TrailDto.kt
package com.example.trailmate.data.remote
import com.example.trailmate.domain.Difficulty
import com.example.trailmate.domain.Trail
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TrailDto(
val id: String,
val name: String,
val region: String,
@SerialName("length_km") val lengthKm: Double,
val difficulty: String,
@SerialName("photo_url") val photoUrl: String? = null,
)
@Serializable
data class TrailPageDto(val items: List<TrailDto>, val page: Int, val hasMore: Boolean)
fun TrailDto.toDomain() = Trail(id, name, region, lengthKm, Difficulty.valueOf(difficulty.uppercase()), photoUrl, isFavorite = false)
DTOs (data transfer objects) are kept separate from the domain Trail so that a server rename never ripples into the
UI. ignoreUnknownKeys = true keeps old app versions working when the server adds fields, a property that matters
because mobile users update slowly. See
Ktor — Content negotiation and serialization in the client and
Kotlin docs — Serialization.
TrailApiClient
The API client is a plain class that takes the HttpClient through its constructor. This dependency injection (DI)
keeps it testable and lets Koin supply the shared client.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/TrailApiClient.kt
package com.example.trailmate.data.remote
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
interface TrailApi {
suspend fun trails(page: Int, region: String?): TrailPageDto
suspend fun trail(id: String): TrailDto
}
class TrailApiClient(private val client: HttpClient) : TrailApi {
override suspend fun trails(page: Int, region: String?): TrailPageDto =
client.get("trails") {
parameter("page", page)
region?.let { parameter("region", it) }
}.body()
override suspend fun trail(id: String): TrailDto =
client.get("trails/$id").body()
}
Three details are worth noticing. The relative path "trails" is resolved against the defaultRequest base URL
https://api.example.com/v1/ (the trailing slash matters, because without it /v1 is dropped; it makes the resolution append rather than replace the last
segment). parameter() URL-encodes values, so never concatenate query strings by hand. And .body() is a reified
function that deserializes using the installed ContentNegotiation. See
Ktor — Making requests and
Ktor — Receiving responses.
defaultRequest
defaultRequest { } applies settings to every request the client makes: the base URL, shared headers and the content
type. It removes repeated boilerplate and gives one place to change the API host between environments.
The block above already sets the base URL and Content-Type; shared headers such as header("X-Client", "trailmate-kmp")
and Accept-Language go in the same block, and url { protocol = URLProtocol.HTTPS; host = "api.example.com" } is the
structured alternative to a string URL.
A request can still override any default (for instance to call a different host for an image CDN). If the base URL
comes from build configuration, pass it into HttpClientFactory.create(baseUrl, engine) rather than reading globals.
See Ktor — Default request.
Timeouts
Without HttpTimeout, behaviour depends on each engine’s defaults, which differ between OkHttp, NSURLSession and
fetch. Installing the plugin gives one consistent policy.
The plugin has three limits: connectTimeoutMillis (establishing the connection, typically 10 s),
requestTimeoutMillis (the whole call, typically 30 s) and socketTimeoutMillis (maximum silence between packets,
typically 30 s).
A timeout throws HttpRequestTimeoutException, ConnectTimeoutException or SocketTimeoutException. The plugin is
Stable on all targets, but note that on the Js engine the connect and socket timeouts are not supported by the
browser fetch API, so only requestTimeoutMillis applies there. Timeouts can also be set for a single request
with timeout { requestTimeoutMillis = 120_000 }, which is the right tool for large downloads. See
Ktor — Timeout.
Retries
HttpRequestRetry re-sends requests that failed because of a server error or an exception. Retrying blindly is
dangerous (a repeated POST can create duplicates), so decide per request.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/HttpClientFactory.kt (excerpt)
import io.ktor.client.plugins.HttpRequestRetry
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 3, retryOnTimeout = true)
exponentialDelay(base = 2.0, maxDelayMs = 10_000)
}
exponentialDelay waits longer after each failure, with jitter, so many clients do not retry in lockstep against a
recovering server. A single request opts out with retry { noRetry() }. Retries multiply the total wall-clock time,
so size requestTimeoutMillis accordingly or the caller waits minutes before seeing an error. See
Ktor — Retrying failed requests.
Logging
The Logging plugin prints requests and responses. Logger.SIMPLE prints to the platform’s standard output, which on
iOS shows up in the Xcode console. A custom Logger forwards lines to a logging library such as Kermit.
A custom logger is logger = object : Logger { override fun log(message: String) = AppLog.d("Http", message) }, and
sanitizeHeader { it == HttpHeaders.Authorization } redacts the token from the output.
Levels run from ALL to NONE. Never log bodies or tokens in release builds; use
sanitizeHeader to redact Authorization. See Ktor — Logging.
Authentication with Bearer Tokens and Refresh
The Auth plugin with the bearer provider adds an Authorization: Bearer <token> header and, when the server
answers 401 Unauthorized, calls refreshTokens once and replays the request. This logic is identical on every
platform, which is a strong argument to keep it in common code.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/AuthInstaller.kt
package com.example.trailmate.data.remote
import io.ktor.client.HttpClientConfig
import io.ktor.client.call.body
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import kotlinx.serialization.Serializable
interface TokenStore {
suspend fun load(): BearerTokens?
suspend fun save(tokens: BearerTokens)
}
@Serializable
private data class RefreshRequest(val refreshToken: String)
@Serializable
private data class TokenResponse(val accessToken: String, val refreshToken: String)
fun HttpClientConfig<*>.installAuth(store: TokenStore) {
install(Auth) {
bearer {
loadTokens { store.load() }
refreshTokens {
val old = oldTokens ?: return@refreshTokens null
val fresh: TokenResponse = client.post("auth/refresh") {
markAsRefreshTokenRequest()
setBody(RefreshRequest(old.refreshToken))
}.body()
BearerTokens(fresh.accessToken, fresh.refreshToken).also { store.save(it) }
}
sendWithoutRequest { request -> request.url.host == "api.example.com" }
}
}
}
markAsRefreshTokenRequest() tells the plugin that this call must not itself trigger a refresh, which avoids an
infinite loop. sendWithoutRequest limits the header to your own host, so the token is not leaked to third-party
URLs such as image CDNs. Store tokens in the platform keystore (Android Keystore, iOS Keychain) behind
TokenStore, never in plain preferences; the tokens in this snippet are placeholders supplied at runtime. Ktor
caches the loaded tokens in memory until a refresh happens, so call the clearToken() extension of the provider
on logout. See Ktor — Bearer authentication.
Status Codes and Error Handling
By default Ktor does not throw for 4xx and 5xx responses; client.get(…) returns a response and .body() fails
later with a confusing deserialization error. Setting expectSuccess = true turns non-2xx statuses into exceptions:
The exceptions are RedirectResponseException (3xx), ClientRequestException (4xx), ServerResponseException (5xx), all
subclasses of ResponseException, plus HttpRequestTimeoutException and platform-specific I/O errors for a network that is
down.
Because engine exceptions differ per platform, wrap them in your own sealed error type at the data boundary so that the ViewModel handles one hierarchy.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/ApiResult.kt
package com.example.trailmate.data.remote
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.ServerResponseException
import io.ktor.client.plugins.HttpRequestTimeoutException
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.CancellationException
sealed interface ApiError {
data object Network : ApiError
data object Timeout : ApiError
data object Unauthorized : ApiError
data class Http(val status: Int) : ApiError
data class Unknown(val cause: Throwable) : ApiError
}
suspend fun <T> apiCall(block: suspend () -> T): Result<T> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e // never swallow cancellation
} catch (e: ClientRequestException) {
Result.failure(ApiException(
if (e.response.status == HttpStatusCode.Unauthorized) ApiError.Unauthorized
else ApiError.Http(e.response.status.value)))
} catch (e: ServerResponseException) {
Result.failure(ApiException(ApiError.Http(e.response.status.value)))
} catch (e: HttpRequestTimeoutException) {
Result.failure(ApiException(ApiError.Timeout))
} catch (e: Throwable) {
Result.failure(ApiException(ApiError.Network))
}
class ApiException(val error: ApiError) : Exception()
Rethrowing CancellationException is essential: swallowing it breaks structured concurrency, so a cancelled screen
would keep running work. For server-defined error bodies, install a HttpResponseValidator { handleResponseExceptionWithRequest { … } }
that reads the JSON error payload once for the whole client. See
Ktor — Response validation.
WebSockets
For real-time features such as live trail conditions, Ktor provides a WebSocket client. Install the plugin and open a session; frames arrive as a coroutine-friendly stream.
// sharedLogic/src/commonMain/kotlin/com/example/trailmate/data/remote/ConditionsSocket.kt
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
fun conditions(client: HttpClient, trailId: String): Flow<String> = flow {
client.webSocket("wss://api.example.com/v1/trails/$trailId/conditions") {
for (frame in incoming) if (frame is Frame.Text) emit(frame.readText())
}
}
This requires install(WebSockets) and the ktor-client-websockets artifact. Support varies by engine: OkHttp,
Darwin, CIO, Java and Js support it, but the Darwin engine exposes WebSockets through NSURLSession and does
not accept every extension. Server-Sent Events (SSE) is a simpler alternative for one-way streams. See
Ktor — WebSockets in the client.
Testing with MockEngine
Because the engine is injected, tests replace it with MockEngine, which answers from a lambda without any network.
This is also how FakeTrailApiClient is built. Tests run in commonTest, so the same suite executes on JVM, iOS
simulator and the other targets.
// sharedLogic/src/commonTest/kotlin/com/example/trailmate/data/remote/FakeTrailApiClient.kt
package com.example.trailmate.data.remote
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
fun fakeTrailApiClient(
status: HttpStatusCode = HttpStatusCode.OK,
body: String = TRAILS_JSON,
): TrailApiClient {
val engine = MockEngine { request ->
check(request.url.encodedPath.endsWith("/trails")) { "Unexpected ${request.url}" }
respond(
content = body,
status = status,
headers = headersOf(HttpHeaders.ContentType, "application/json"),
)
}
return TrailApiClient(HttpClientFactory.create(engine))
}
private const val TRAILS_JSON =
"""{"items":[{"id":"t1","name":"Ridge Loop","region":"Alps","length_km":8.4,"difficulty":"moderate"}],"page":1,"hasMore":false}"""
// sharedLogic/src/commonTest/kotlin/com/example/trailmate/data/remote/TrailApiClientTest.kt
package com.example.trailmate.data.remote
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class TrailApiClientTest {
@Test
fun parsesTrailPage() = runTest {
val page = fakeTrailApiClient().trails(page = 1, region = null)
assertEquals("Ridge Loop", page.items.single().name)
}
}
Using the real HttpClientFactory with a MockEngine tests the configured client (serialization, base URL,
expectSuccess) and not just your code. The mock engine is Stable and is the recommended approach; Mokkery-style
mocks of HttpClient are fragile because HttpClient is a large concrete class. See
Ktor — Testing and
Testing Multiplatform Code.
Wiring It with Koin
The client is a Koin singleton, single { HttpClientFactory.create(httpClientEngine()) }, and TrailApiClient is bound to
TrailApi with single<TrailApi> { TrailApiClient(get()) } inside trailMateModule; see
Dependency Injection in KMP: Koin, Metro and Manual DI
and Koin — Koin for Kotlin Multiplatform.
Using the Client from Swift
The iOS app never touches Ktor directly; it calls the repository or observes the shared ViewModel. A suspend function
exported to Swift becomes async throws with SKIE, and a completion-handler function without it.
// iosApp/iosApp/TrailListView.swift (excerpt)
import SwiftUI
import SharedLogic
struct TrailListView: View {
let repository: TrailRepository // provided by Koin on the Kotlin side
@State private var message = "Loading..."
var body: some View {
Text(message).task {
do {
let trails = try await repository.refresh(page: 1)
message = "\(trails.count) trails"
} catch {
message = "Failed: \(error.localizedDescription)"
}
}
}
}
Without SKIE a suspend function must be annotated with @Throws(CancellationException::class) or an uncaught
Kotlin exception terminates the iOS process. See
Consuming Coroutines and Flow from Swift and SwiftUI
and Objective-C Interop, Swift Export and SKIE.
See SKIE — Swift Kotlin Interface Enhancer.
Platform Behaviour
On Android declare android.permission.INTERNET in the app manifest (androidApp) and remember that cleartext HTTP
is blocked by default. On iOS, App Transport Security requires HTTPS. In the browser the API must send CORS headers
for your origin. Desktop has no permission model.
Migrating from Ktor 1.x to 3.x
Older tutorials, including many from the Kotlin Multiplatform Mobile era, use Ktor 1.x. The code does not compile on Ktor 3.x; these are the changes you will meet.
| Ktor 1.x | Ktor 3.x |
|---|---|
|
|
|
|
Package |
Package |
|
|
Native |
Not needed: new Kotlin/Native memory manager and Gradle module metadata |
No Wasm support |
|
Migrate by first replacing JsonFeature and the artifact names, then fix imports from features to
plugins, then change get<T>() calls to .body<T>(). Ktor 3.0 additionally changed engine configuration types
and removed several deprecated APIs from 2.x; the official migration guides list each. See
Ktor — Migrating from 1.x to 2.x and
Ktor — Migrating from 2.x to 3.x.
Common Mistakes
-
Creating an
HttpClientper request. Each client has its own connection pool; share one and close it at shutdown. -
Forgetting the engine on a target.
HttpClient()fails at runtime with "Failed to find HTTP client engine implementation"; add one engine dependency per target source set. -
Leaving
expectSuccessoff. A 500 returns an HTML body that fails deserialization with a misleading message. -
Sending the bearer token to every host. Restrict it with
sendWithoutRequest. -
Using Gson or Jackson converters (JVM-only), or forgetting
@Serializable. -
Retrying non-idempotent requests or logging bodies in production.
-
Swallowing
CancellationExceptionin a broadcatch (e: Exception).