diff --git a/lib/dialogs/error_dialog.dart b/lib/dialogs/error_dialog.dart
new file mode 100644
index 0000000000000000000000000000000000000000..083491c1afc5ba120a2e865e06f67fca07204e33
--- /dev/null
+++ b/lib/dialogs/error_dialog.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+
+class ErrorDialog extends StatelessWidget {
+  final String title;
+  final Object? error;
+
+  const ErrorDialog({super.key, required this.title, required this.error});
+
+  @override
+  Widget build(BuildContext context) {
+    return SimpleDialog(
+      title: Text(title),
+      children: [
+        SimpleDialogOption(
+          child: Text('An error occurred: ${error.runtimeType}\n$error'),
+        ),
+      ],
+    );
+  }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 9fd493976f7f5858fc4c9896d87f8612edff55fd..3fd81e6e3263b4feb9eaeeb8b0068848402fe833 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,9 +1,11 @@
 import 'dart:async';
+import 'dart:io';
 
 import 'package:flutter/material.dart';
 
 import 'branding.dart';
 import 'context.dart';
+import 'dialogs/error_dialog.dart';
 import 'drawer/drawer.dart';
 import 'timetable/timetable.dart';
 import 'toolbar/toolbar.dart';
@@ -63,7 +65,9 @@ class _MyHomePageState extends State<MyHomePage> {
   @override
   void initState() {
     super.initState();
-    loadProfile();
+    loadProfile().onError((error, _) {
+      _showErrorDialog('Failed to connect', error);
+    });
   }
 
   @override
@@ -74,6 +78,14 @@ class _MyHomePageState extends State<MyHomePage> {
     super.dispose();
   }
 
+  void _showErrorDialog(String title, Object? error) {
+    stderr.write(error);
+    showDialog(
+      context: context,
+      builder: (context) => ErrorDialog(title: title, error: error),
+    );
+  }
+
   Future<void> loadProfile() async {
     final profile = await appContext.profile.firstNotNull;
     final classes = await untis.allClasses.fetched;
diff --git a/lib/untis/fetch.dart b/lib/untis/fetch.dart
index e993038985e1d6e5fc065ecf82de83a2009148f6..793fc15dae31f1c1e7af85dd48f728f8caee8b30 100644
--- a/lib/untis/fetch.dart
+++ b/lib/untis/fetch.dart
@@ -84,14 +84,21 @@ class UntisFetch {
     final responseString = await utf8.decodeStream(response);
     final onDownloadEnd = DateTime.now();
 
-    final bodyJson = jsonDecode(responseString);
+    if (response.statusCode != 200) {
+      throw FetchError(response.statusCode, responseString);
+    }
 
-    final onDecodeEnd = DateTime.now();
+    try {
+      final bodyJson = jsonDecode(responseString);
+      final onDecodeEnd = DateTime.now();
 
-    final stats =
-        FetchStats(onStart, onDownloadStart, onDownloadEnd, onDecodeEnd);
+      final stats =
+          FetchStats(onStart, onDownloadStart, onDownloadEnd, onDecodeEnd);
 
-    return (bodyJson as Json, resultCookies, stats);
+      return (bodyJson as Json, resultCookies, stats);
+    } on FormatException catch (_) {
+      throw FetchError(response.statusCode, responseString);
+    }
   }
 
   Future<Json> requestOptions(RequestOptions options) async {
@@ -148,3 +155,13 @@ class FetchStats {
         .join(', ');
   }
 }
+
+class FetchError extends Error {
+  final int statusCode;
+  final String body;
+
+  FetchError(this.statusCode, this.body);
+
+  @override
+  String toString() => 'Server responded with status code $statusCode\n$body';
+}