diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/ErrorScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/ErrorScreen.kt
new file mode 100644
index 0000000000000000000000000000000000000000..47dc19d0552bfb72dced3fe629f32f2e0e66bb74
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/ErrorScreen.kt
@@ -0,0 +1,81 @@
+package org.briarproject.briar.desktop.login
+
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS
+import org.briarproject.briar.desktop.theme.Red500
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.PreviewUtils.preview
+
+fun main() = preview {
+    var error by remember { mutableStateOf(SUCCESS) }
+
+    Row(horizontalArrangement = spacedBy(8.dp)) {
+        for (e in StartResult.values().filterNot { it in listOf(SUCCESS, ALREADY_RUNNING) }) {
+            Button(onClick = { error = e }) {
+                Text(e.name.removeSuffix("_ERROR"))
+            }
+        }
+    }
+
+    ErrorScreen(error)
+}
+
+@Composable
+fun ErrorScreen(error: StartResult) = Surface {
+    Column(
+        modifier = Modifier.fillMaxSize().padding(32.dp),
+        horizontalAlignment = CenterHorizontally,
+        verticalArrangement = spacedBy(32.dp)
+    ) {
+        Icon(
+            imageVector = Icons.Filled.Error,
+            contentDescription = i18n("error"),
+            modifier = Modifier.size(128.dp),
+            tint = Red500
+        )
+
+        Text(i18n("sorry"), style = MaterialTheme.typography.h5)
+
+        val text = when (error) {
+            CLOCK_ERROR -> i18n("startup.failed.clock_error")
+            DB_ERROR -> i18n("startup.failed.db_error")
+            DATA_TOO_OLD_ERROR -> i18n("startup.failed.data_too_old_error")
+            DATA_TOO_NEW_ERROR -> i18n("startup.failed.data_too_new_error")
+            SERVICE_ERROR -> i18n("startup.failed.service_error")
+            else -> ""
+        }
+        Text(
+            text = text,
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier.widthIn(max = 400.dp)
+        )
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
index 3104a56e3a7aafb702d1ccf2ae6ef817e7b7d120..576026e5b15d7bf4da2dad084fcbdfaf5d8046c6 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
@@ -33,7 +33,7 @@ import org.briarproject.briar.desktop.viewmodel.viewModel
 @Composable
 fun LoginScreen(
     viewModel: LoginViewModel = viewModel(),
-) = StartupScreen(i18n("startup.title.login")) { // todo: i18n
+) = StartupScreen(i18n("startup.title.login")) {
     when (viewModel.state.value) {
         SIGNED_OUT ->
             FormScaffold(
@@ -58,6 +58,7 @@ fun LoginScreen(
 
     if (viewModel.decryptionFailedError.value) {
         // todo: is this actually needed on Desktop?
+        // todo: use ErrorScreen to display this instead
         AlertDialog(
             onDismissRequest = viewModel::closeDecryptionFailedDialog,
             title = { Text(i18n("startup.error.decryption.title")) },
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
index a7887c4281d5da9f73eb1e39fe2df033fa80622a..418e0b5ba10a2e594d619989eda8d83ab0202a69 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
@@ -9,7 +9,6 @@ import org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERR
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
-import org.briarproject.bramble.api.lifecycle.IoExecutor
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState
 import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent
@@ -18,6 +17,7 @@ import org.briarproject.briar.desktop.login.LoginViewModel.State.MIGRATING
 import org.briarproject.briar.desktop.login.LoginViewModel.State.SIGNED_OUT
 import org.briarproject.briar.desktop.login.LoginViewModel.State.STARTED
 import org.briarproject.briar.desktop.login.LoginViewModel.State.STARTING
+import org.briarproject.briar.desktop.login.StartupUtils.startBriarCore
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
@@ -85,13 +85,6 @@ constructor(
             }
     }
 
-    @IoExecutor
-    private fun signedIn() {
-        val dbKey = accountManager.databaseKey ?: throw AssertionError()
-        lifecycleManager.startServices(dbKey)
-        lifecycleManager.waitForStartup()
-    }
-
     fun deleteAccount() = briarExecutors.onIoThread {
         accountManager.deleteAccount()
         eventBus.broadcast(AccountDeletedEvent())
@@ -104,7 +97,7 @@ constructor(
         briarExecutors.onIoThread {
             try {
                 accountManager.signIn(password.value)
-                signedIn()
+                startBriarCore(accountManager, lifecycleManager, eventBus)
             } catch (e: DecryptionException) {
                 // failure, try again
                 briarExecutors.onUiThread {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
index e83367336aeb4197f6cb2af176e3d8f130310991..a72d5a9e85072789659072f61dab1346cb59453d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
@@ -6,13 +6,14 @@ import mu.KotlinLogging
 import org.briarproject.bramble.api.account.AccountManager
 import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
 import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
+import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
-import org.briarproject.bramble.api.lifecycle.IoExecutor
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.briar.desktop.login.RegistrationViewModel.State.CREATED
 import org.briarproject.briar.desktop.login.RegistrationViewModel.State.CREATING
 import org.briarproject.briar.desktop.login.RegistrationViewModel.State.INSERT_NICKNAME
 import org.briarproject.briar.desktop.login.RegistrationViewModel.State.INSERT_PASSWORD
+import org.briarproject.briar.desktop.login.StartupUtils.startBriarCore
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.viewmodel.ViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
@@ -24,7 +25,8 @@ constructor(
     private val accountManager: AccountManager,
     private val briarExecutors: BriarExecutors,
     private val lifecycleManager: LifecycleManager,
-    private val passwordStrengthEstimator: PasswordStrengthEstimator
+    private val passwordStrengthEstimator: PasswordStrengthEstimator,
+    private val eventBus: EventBus,
 ) : ViewModel {
 
     companion object {
@@ -87,13 +89,6 @@ constructor(
         _passwordConfirmation.value = passwordConfirmation
     }
 
-    @IoExecutor
-    private fun signedIn() { // todo: factor out?
-        val dbKey = accountManager.databaseKey ?: throw AssertionError()
-        lifecycleManager.startServices(dbKey)
-        lifecycleManager.waitForStartup()
-    }
-
     fun goToPassword() {
         if (!buttonEnabled.value) return
 
@@ -111,7 +106,7 @@ constructor(
         briarExecutors.onIoThread {
             if (accountManager.createAccount(_nickname.value, _password.value)) {
                 LOG.info { "Created account" }
-                signedIn()
+                startBriarCore(accountManager, lifecycleManager, eventBus)
                 _state.value = CREATED
             } else {
                 LOG.warn { "Failed to create account" }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/StartupFailedEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/StartupFailedEvent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1976843ebced37832cfdc103cbb40cd376c4af87
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/StartupFailedEvent.kt
@@ -0,0 +1,6 @@
+package org.briarproject.briar.desktop.login
+
+import org.briarproject.bramble.api.event.Event
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+
+class StartupFailedEvent(val result: LifecycleManager.StartResult) : Event()
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/StartupUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/StartupUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cbb1dc1460db44c3121e095980dc07452a4f14a2
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/StartupUtils.kt
@@ -0,0 +1,28 @@
+package org.briarproject.briar.desktop.login
+
+import mu.KotlinLogging
+import org.briarproject.bramble.api.account.AccountManager
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.bramble.api.lifecycle.IoExecutor
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS
+
+object StartupUtils {
+
+    private val LOG = KotlinLogging.logger {}
+
+    @IoExecutor
+    fun startBriarCore(accountManager: AccountManager, lifecycleManager: LifecycleManager, eventBus: EventBus) {
+        val dbKey = accountManager.databaseKey ?: throw AssertionError()
+        val result = lifecycleManager.startServices(dbKey)
+        when (result) {
+            SUCCESS -> lifecycleManager.waitForStartup()
+            ALREADY_RUNNING -> LOG.info { "Already running" }
+            else -> {
+                LOG.warn { "Startup failed: $result" }
+                eventBus.broadcast(StartupFailedEvent(result))
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 445e1a526a86395ff7e3b29a59414c9c9061e4e7..2ec6a6949cf298ecc32b6f4ada4250bcfb9cf364 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -15,16 +15,20 @@ import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult
 import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent
 import org.briarproject.briar.desktop.DesktopFeatureFlags
 import org.briarproject.briar.desktop.login.AccountDeletedEvent
+import org.briarproject.briar.desktop.login.ErrorScreen
 import org.briarproject.briar.desktop.login.LoginScreen
 import org.briarproject.briar.desktop.login.RegistrationScreen
+import org.briarproject.briar.desktop.login.StartupFailedEvent
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import org.briarproject.briar.desktop.theme.BriarTheme
 import org.briarproject.briar.desktop.ui.Screen.LOGIN
 import org.briarproject.briar.desktop.ui.Screen.MAIN
 import org.briarproject.briar.desktop.ui.Screen.REGISTRATION
+import org.briarproject.briar.desktop.ui.Screen.STARTUP_ERROR
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.viewmodel.ViewModelProvider
 import org.briarproject.briar.desktop.viewmodel.viewModel
@@ -33,10 +37,11 @@ import javax.annotation.concurrent.Immutable
 import javax.inject.Inject
 import javax.inject.Singleton
 
-enum class Screen {
-    REGISTRATION,
-    LOGIN,
-    MAIN
+sealed interface Screen {
+    object REGISTRATION : Screen
+    object LOGIN : Screen
+    object MAIN : Screen
+    class STARTUP_ERROR(val error: StartResult) : Screen
 }
 
 interface BriarUi {
@@ -71,10 +76,13 @@ constructor(
     )
 
     override fun eventOccurred(e: Event?) {
-        if (e is LifecycleEvent && e.lifecycleState == RUNNING) {
-            screenState = MAIN
-        } else if (e is AccountDeletedEvent) {
-            screenState = REGISTRATION
+        when {
+            e is LifecycleEvent && e.lifecycleState == RUNNING ->
+                screenState = MAIN
+            e is AccountDeletedEvent ->
+                screenState = REGISTRATION
+            e is StartupFailedEvent ->
+                screenState = STARTUP_ERROR(e.result)
         }
     }
 
@@ -104,10 +112,11 @@ constructor(
             ) {
                 val settingsViewModel: SettingsViewModel = viewModel()
                 BriarTheme(isDarkTheme = settingsViewModel.isDarkMode.value) {
-                    when (screenState) {
-                        REGISTRATION -> RegistrationScreen()
-                        LOGIN -> LoginScreen()
-                        MAIN -> MainScreen(settingsViewModel)
+                    when (val state = screenState) {
+                        is REGISTRATION -> RegistrationScreen()
+                        is LOGIN -> LoginScreen()
+                        is STARTUP_ERROR -> ErrorScreen(state.error)
+                        is MAIN -> MainScreen(settingsViewModel)
                     }
                 }
             }
diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties
index c43b7a2d57678d0c6cf4029f66dea15c4751807a..39f99f3c2461a66dd78c12f2794e95b59fc3ce9c 100644
--- a/src/main/resources/strings/BriarDesktop.properties
+++ b/src/main/resources/strings/BriarDesktop.properties
@@ -116,6 +116,8 @@ decline=Decline
 back=Back
 next=Next
 open=Open
+sorry=Sorry
+error=Error
 unsupported_feature=Unfortunately, this feature is not yet supported by Briar Desktop.
 
 # Startup screen
@@ -137,6 +139,11 @@ startup.error.decryption.text=Briar cannot check your password. Please try reboo
 startup.password_forgotten.button=I have forgotten my password
 startup.password_forgotten.title=Lost Password
 startup.password_forgotten.text=Your Briar account is stored encrypted on your device, not in the cloud, so we can't reset your password. Would you like to delete your account and start again?\n\nCaution: Your identities, contacts and messages will be permanently lost.
+startup.failed.clock_error=Briar was unable to start because your device's clock is wrong.\n\nPlease set your device's clock to the right time and try again.
+startup.failed.db_error=Briar was unable to open the database containing your account, your contacts and your messages.\n\nPlease upgrade to the latest version of the app and try again, or set up a new account by choosing 'I have forgotten my password' at the password prompt.
+startup.failed.data_too_old_error=Your account was created with an old version of this app and cannot be opened with this version.\n\nYou must either reinstall the old version or set up a new account by choosing 'I have forgotten my password' at the password prompt.
+startup.failed.data_too_new_error=Your account was created with a newer version of this app and cannot be opened with this version.\n\nPlease upgrade to the latest version and try again.
+startup.failed.service_error=Briar was unable to start a required component.\n\nPlease upgrade to the latest version of the app and try again.
 startup.database.creating=Creating Account...
 startup.database.opening=Decrypting Database...
 startup.database.migrating=Upgrading Database...