Coroutines and Error Handling - Android
Most actions in the Android SDK are Kotlin suspend functions. This includes join(), leave(), the mic, camera and screen share controls, recording and livestream controls, the PubSub methods, and the actions on a Participant or an RTCStream. This guide shows how to call them and how to handle the errors they report.
Calling actions from a coroutine
A suspend function can only be called from a coroutine. Launch one from a scope that matches the screen or class that owns the room:
- In an
ActivityorFragment, uselifecycleScope, fromandroidx.lifecycle:lifecycle-runtime-ktx. - In a
ViewModel, useviewModelScope, fromandroidx.lifecycle:lifecycle-viewmodel-ktx. - In Jetpack Compose, use the scope from
rememberCoroutineScope().
The SDK brings kotlinx-coroutines-android with it, so you only need to add the library that provides the scope.
dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
// only if you call the SDK from a ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
}
You can call these functions from the main thread. They don't block it while they wait for the SDK.
class RoomActivity : AppCompatActivity() {
private lateinit var room: Room
private fun toggleMic() {
lifecycleScope.launch {
try {
if (room.isMicEnabled) room.muteMic() else room.unmuteMic()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Mic toggle failed: ${e.code} ${e.name}")
}
}
}
}
In a ViewModel, the same call looks like this:
class RoomViewModel(private val room: Room) : ViewModel() {
fun toggleCam() {
viewModelScope.launch {
try {
if (room.isCamEnabled) room.disableCam() else room.enableCam()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Camera toggle failed: ${e.code} ${e.name}")
}
}
}
}
- A
suspendfunction returns when the SDK has handled your request. A request that the server carries out, such asstartRecording(), can return before the work is done. Wait for its event, for exampleonRecordingStateChanged(), before you update the UI. - A scope is cancelled when its owner goes away:
lifecycleScopewhen the activity is destroyed,viewModelScopewhen theViewModelis cleared. A coroutine launched fromonDestroy()never runs. Leave the room before the screen closes, as shown in Leave or End Room.
The track factories VideoSDK.createCameraVideoTrack() and VideoSDK.createMicrophoneAudioTrack() are also suspend functions. They return once the device is open, and throw a VideoSDKException if it cannot be opened.
The PubSub methods publish(), subscribe() and unsubscribe() are suspend functions too. They return once the server has answered, and throw a VideoSDKException on failure.
Some calls are not suspend functions but can still throw VideoSDKException, for example VideoSDK.createRoom() and room.setAudioDevice(). Wrap them in try/catch too.
Handling VideoSDKException
When an action is refused or fails, it throws a VideoSDKException. It is an unchecked exception, so the compiler does not remind you to catch it. An exception you don't catch crashes the app.
VideoSDKException has these properties:
| Property | Type | Description |
|---|---|---|
code | Int | The error code, for example 3054. |
name | String | The error name, for example "ERROR_ALREADY_IN_REQUESTED_STATE". |
message | String? | A readable reason. Log it, but don't branch on it, because the server can change the text. |
error | VideoSDKError | The failure as a VideoSDKError, with this exception's code, name and message. |
Branch on error, or on code, to decide what to do. Don't branch on name: names can change between releases. A VideoSDKError equals its constant when the codes match, so compare with == or when, never with ===. A when used as an expression needs an else branch.
lifecycleScope.launch {
try {
room.enableCam()
} catch (e: VideoSDKException) {
when (e.error) {
VideoSDKError.ERROR_ALREADY_IN_REQUESTED_STATE -> Unit // the camera is already on
VideoSDKError.ERROR_OPERATION_IN_PROGRESS -> Unit // a camera toggle is still running
VideoSDKError.ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED -> showPermissionHelp()
else -> Log.e("VideoSDK", "Camera failed: ${e.code} ${e.name} ${e.message}")
}
}
}
Errors reported on onError
Errors also reach the onError() event of RoomEventListener as a VideoSDKError with code, name and message. It equals its VideoSDKError constant when the codes match, so you can branch on it with when. Add an else branch for every other error.
private val roomEventListener = object : RoomEventListener() {
override fun onError(error: VideoSDKError) {
when (error) {
VideoSDKError.ERROR_ROOM_RECONNECTING -> Unit // wait for onRoomStateChanged
else -> Log.e("VideoSDK", "onError: ${error.code} ${error.name} ${error.message}")
}
// onError can arrive off the main thread
runOnUiThread { showError(error.message) }
}
}
An error can reach your app in three ways:
- Thrown and reported. Most errors are thrown by the call that failed and also reported on
onError. - Thrown only. Some errors describe a mistake in the call itself and are only thrown, for example
3048(ERROR_MEDIA_NOT_READY),3066(ERROR_ACTION_NOT_SUPPORTED_IN_MODE),3072(ERROR_PAYLOAD_TOO_LARGE) and3073(ERROR_INVALID_PARAMETER). Catch them where you make the call. - Reported only. Problems that happen after a call has returned, such as a camera that stops working during the call or a recording that fails on the server, are only reported on
onError.
Calls made before joining or while reconnecting
Most actions need a joined room. Called before join() completes, they report 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) on onError. Called while the room is reconnecting, they report 3065 (ERROR_ROOM_RECONNECTING). Either way, nothing is sent and nothing changes. Participant methods are the exception during a join: called while join() is still running, they wait for it to finish instead of reporting 3035.
Many actions, such as unmuteMic(), enableCam() and startRecording(), then return without throwing. Others also throw a VideoSDKException with the same code, for example changeMic(), changeCam(), uploadBase64File(), fetchBase64File(), the PubSub methods and RTCStream.pause() / resume(). While the room is reconnecting, pubSub.publish() and pubSub.subscribe() throw 3065 without reporting it on onError. Each method's API reference lists what it throws.
A call that is refused while the room reconnects is not repeated afterwards. Make the call again once onRoomStateChanged() reports CONNECTED.
Threads
RoomEventListener,ParticipantEventListenerandPubSubMessageListenercallbacks run on the main thread, so you can update views from them. One exception: when you callparticipant.addEventListener(),onStreamEnabled()is called right away for each stream the participant already has, on the thread that registers the listener.onError()is the exception: an error raised by a refused call arrives on the thread that made the call, and a camera or microphone failure arrives on a background thread. Post to the main thread before you update the UI.- Catch and log exceptions inside your own listener callbacks. Otherwise a bug in a callback can go unnoticed.
API Reference
The API references for all the methods and events utilized in this guide are provided below.
Got a Question? Ask us on discord

