MAUI Error Handling


Error handling is one of the most important tasks during building a stable .NET MAUI application. It allows proper error handling to let your app go from unexpected hitch-ups and provide a nice user experience.

In .NET MAUI, you can handle errors in the following ways:

  • ViewModel-Level Error Handling: Capturing and managing errors in ViewModels.
  • Global Error Handling: Handling uncaught exceptions globally across the application.
  • Platform-Specific Error Handling: Some platforms, such as Android or iOS, may throw platform-specific exceptions that must be handled.
  • User-Friendly Error Notifications: Also, in cases of errors, notify the users in the least intrusive manner.

ViewModel Level Error Handling

I have covered this topic here.

Global Error Handling

Then, .NET MAUI supports the implementation of Global Error Handling, supposed to catch unhandled exceptions and let the application behave gracefully. That is with event handlers for the unhandled exceptions and for the unobserved task exceptions.

Modify App.xaml.cs to Implement Global Error Handling

using System;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;

namespace MauiAppExample
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            // Setup global error handlers
            AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
            TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;

            MainPage = new NavigationPage(new MainPage());
        }

        // Global handler for unhandled exceptions in non-UI threads
        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is Exception ex)
            {
                LogException(ex);
                ShowErrorMessage("An unexpected error occurred. The application will try to recover.");
            }
        }

        // Global handler for unobserved task exceptions
        private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            LogException(e.Exception);
            ShowErrorMessage("An unexpected error occurred during a background operation.");
            e.SetObserved(); // Prevent the application from crashing due to unobserved task exceptions
        }

        // Method to log the exception
        private void LogException(Exception ex)
        {
            // Log the exception details, optionally use a logging service like AppCenter, etc.
            System.Diagnostics.Debug.WriteLine($"Unhandled Exception: {ex.Message}");
            System.Diagnostics.Debug.WriteLine($"Stack Trace: {ex.StackTrace}");
        }

        // Method to show a user-friendly message
        private void ShowErrorMessage(string message)
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await Current.MainPage.DisplayAlert("Error", message, "OK");
            });
        }
    }
}

Global Handlers Setup:

  • In the constructor of App there are set global handlers for:
    • AppDomain.CurrentDomain.UnhandledException: It catches exceptions that are not handled anywhere.
    • TaskScheduler.UnobservedTaskException: TaskScheduler catches exceptions that were unobserved, which may happen in the course of background operations.

OnUnhandledException Method:

  • This method handles exceptions thrown by those that are not caught.
  • The LogException method logs an error, while the ShowErrorMessage method shows an alert to notify a user.

OnUnobservedTaskException Method:

  • This is how the approach handles exceptions from unobserved tasks.
  • e.SetObserved() ensures that the exception of the task will be “observed,” so the application won’t crash.

LogException Method:

  • Uses System.Diagnostics.Debug.WriteLine to log the exception details of the exception, which could be substituted by any logging library or service you would want to use for actual professional logging, such as Microsoft AppCenter or Serilog.

ShowErrorMessage Method:

  • Displays a user-friendly error message using DisplayAlert.
  • Uses MainThread.BeginInvokeOnMainThread to ensure that UI updates, like showing an alert, are done on the main thread.

Adding Logging Services – Serilog

Install the Serilog NuGet Packages

To start using Serilog you must install or add the appropriate NuGet packages. You will need:

  • Serilog.AspNetCore – Integration
  • Serilog.Sinks.Console – To write log messages to the console.
  • Serilog.Sinks.File – For writing log messages into a file

You can install these using the NuGet.

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Configure Serilog in App.xaml.cs

In your App.xaml.cs file, configure Serilog sothis is available globally in your application.

using Microsoft.Maui.Controls;
using Serilog;

namespace MauiAppExample
{
    public partial class App : Application
    {
        public App()
        {
            // Configure Serilog
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug() // Minimum logging level (can be Information, Error, etc.)
                .WriteTo.Console() // Writes logs to the console
                .WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day) // Writes logs to a file
                .CreateLogger();

            InitializeComponent();

            // Setup global error handlers
            AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
            TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;

            MainPage = new NavigationPage(new MainPage());

            // Log application startup
            Log.Information("Application starting up...");
        }

        protected override void OnStart()
        {
            base.OnStart();
            Log.Information("Application started");
        }

        protected override void OnSleep()
        {
            base.OnSleep();
            Log.Information("Application sleeping");
        }

        protected override void OnResume()
        {
            base.OnResume();
            Log.Information("Application resumed");
        }

        // Global handler for unhandled exceptions in non-UI threads
        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is Exception ex)
            {
                Log.Error(ex, "Unhandled Exception");
                ShowErrorMessage("An unexpected error occurred. The application will try to recover.");
            }
        }

        // Global handler for unobserved task exceptions
        private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            Log.Error(e.Exception, "Unobserved Task Exception");
            ShowErrorMessage("An unexpected error occurred during a background operation.");
            e.SetObserved(); // Prevent the application from crashing due to unobserved task exceptions
        }

        // Method to show a user-friendly message
        private void ShowErrorMessage(string message)
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await Current.MainPage.DisplayAlert("Error", message, "OK");
            });
        }
    }
}

Serilog Configuration:

  • Log.Logger = new LoggerConfiguration(): Initializing the Serilog logger.
  • MinimumLevel.Debug(): Sets the minimum level of logs to be recorded. This could be Information, Warning, Error, etc.
  • WriteTo.Console(): Writes log messages to console output.
  • WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day): The log messages are written to a file with a daily rolling log.

Logging Lifecycle Events:

  • OnStart(), OnSleep(), OnResume(): Log application lifecycle events for more detailed debugging.

Global Exception Handling:

  • The exceptions are logged via Serilog (previously just logging with Debug.WriteLine, now Log.Error(ex, "Unhandled Exception") and Log.Error(e.Exception, "Unobserved Task Exception")).

Add Logging to ViewModels

using System.Collections.ObjectModel;
using System.Windows.Input;
using MauiAppExample.Models;
using Serilog;

namespace MauiAppExample.ViewModels
{
    public class ProductViewModel : BaseViewModel
    {
        private string newProductName;

        public string NewProductName
        {
            get => newProductName;
            set => SetProperty(ref newProductName, value);
        }

        public ObservableCollection<Product> Products { get; } = new ObservableCollection<Product>();

        public ICommand AddProductCommand { get; }

        public ProductViewModel()
        {
            AddProductCommand = new Command(OnAddProduct);
        }

        private void OnAddProduct()
        {
            try
            {
                if (string.IsNullOrWhiteSpace(NewProductName))
                {
                    throw new ArgumentException("Product name cannot be empty.");
                }

                Products.Add(new Product { Name = NewProductName });
                Log.Information("Product added: {ProductName}", NewProductName);

                NewProductName = string.Empty; // Reset the input field after adding the product
            }
            catch (Exception ex)
            {
                HandleError(ex);
                Log.Error(ex, "Error adding product");
            }
        }
    }
}

Platform-Specific Error Handling

You may run into platform-dependent exceptions, such as file access/permissions. You can resolve these using platform conditional compilation.

Create a Service for File Access

To do this, create a service to handle file access. Within this file, add platform-specific methods for reading files.

namespace MauiAppExample.Services
{
    public interface IFileService
    {
        string ReadFile(string filePath);
    }
}

Implement Platform-Specific Code with Conditional Compilation

In the file service implementation, handle the platform-dependent exceptions using platform-specific compilation directives:

using System;
using System.IO;
using MauiAppExample.Services;

namespace MauiAppExample.PlatformServices
{
    public class FileService : IFileService
    {
        public string ReadFile(string filePath)
        {
            try
            {
#if ANDROID
                // Android-specific file access logic
                // Example: Accessing a file that may need permission to read external storage
                if (!File.Exists(filePath))
                {
                    throw new FileNotFoundException("File not found on Android device.");
                }
                return File.ReadAllText(filePath);

#elif IOS
                // iOS-specific file access logic
                // Example: Accessing a file in the app's Documents directory
                var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                var fullPath = Path.Combine(documentsPath, filePath);
                if (!File.Exists(fullPath))
                {
                    throw new FileNotFoundException("File not found in iOS documents directory.");
                }
                return File.ReadAllText(fullPath);

#elif WINDOWS
                // Windows-specific file access logic
                // Example: Checking if the file is in the local storage
                if (!File.Exists(filePath))
                {
                    throw new FileNotFoundException("File not found on Windows.");
                }
                return File.ReadAllText(filePath);

#else
                throw new PlatformNotSupportedException("File reading is not supported on this platform.");
#endif
            }
            catch (UnauthorizedAccessException ex)
            {
                // Handle platform-specific permissions issues
                System.Diagnostics.Debug.WriteLine($"Permission error: {ex.Message}");
                return $"Permission error: {ex.Message}";
            }
            catch (Exception ex)
            {
                // Generic error handling
                System.Diagnostics.Debug.WriteLine($"An error occurred: {ex.Message}");
                return $"An error occurred: {ex.Message}";
            }
        }
    }
}

Conditional Compilation (#if ANDROID, #elif IOS, #elif WINDOWS):

  • This would allow you to explicitly mention platform-specific code and handle those specific platform-specific exceptions.

Android-Specific Code:

  • Checks whether the given file exists in an Android device.
  • May throw a FileNotFoundException if the file is missing.

iOS-Specific Code:

  • Reads the file using the Documents directory of the app, which is common for iOS applications.
  • Throws a FileNotFoundException if the file does not exist.

Windows-Specific Code:

  • Handles Windows-specific file access logic.

Error Handling:

  • UnauthorizedAccessException:This handles permission-related exceptions, such as accessing a restricted file.
  • Generic Exception Handling: Catches other general exceptions.

Register the FileService in Dependency Injection

You have to register FileService for Dependency injection to be able to use it inside the application.

using Microsoft.Maui.Hosting;
using MauiAppExample.PlatformServices;
using MauiAppExample.Services;

namespace MauiAppExample
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                });

            // Register the FileService with the Dependency Injection container
            builder.Services.AddSingleton<IFileService, FileService>();

            return builder.Build();
        }
    }
}

Use the FileService in a ViewModel

You can inject IFileService in a ViewModel and use it to read a file. This will handle platform-specific file access gracefully.

using MauiAppExample.Services;
using System.Windows.Input;
using Microsoft.Maui.Controls;

namespace MauiAppExample.ViewModels
{
    public class FileViewModel : BaseViewModel
    {
        private readonly IFileService _fileService;
        private string fileContent;

        public string FileContent
        {
            get => fileContent;
            set => SetProperty(ref fileContent, value);
        }

        public ICommand ReadFileCommand { get; }

        public FileViewModel(IFileService fileService)
        {
            _fileService = fileService;
            ReadFileCommand = new Command(OnReadFile);
        }

        private void OnReadFile()
        {
            try
            {
                // Replace "example.txt" with the actual file path
                FileContent = _fileService.ReadFile("example.txt");
            }
            catch (Exception ex)
            {
                FileContent = $"An error occurred: {ex.Message}";
            }
        }
    }
}

Use the ViewModel in a View

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiAppExample.ViewModels"
             x:Class="MauiAppExample.MainPage">

    <ContentPage.BindingContext>
        <local:FileViewModel />
    </ContentPage.BindingContext>

    <StackLayout Padding="20" Spacing="15">
        <!-- Button to Read File -->
        <Button Text="Read File"
                Command="{Binding ReadFileCommand}"
                FontSize="18" />

        <!-- Display File Content or Error -->
        <Label Text="{Binding FileContent}"
               FontSize="14"
               TextColor="Black" />
    </StackLayout>
</ContentPage>

User-Friendly Error Notifications

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiAppExample.ViewModels"
             x:Class="MauiAppExample.MainPage">

    <ContentPage.BindingContext>
        <local:ProductViewModel />
    </ContentPage.BindingContext>

    <StackLayout Padding="20" Spacing="15">
        <!-- Entry for Product Name -->
        <Entry Placeholder="Enter product name"
               Text="{Binding NewProductName}"
               FontSize="18" />

        <!-- Button to Add Product -->
        <Button Text="Add Product"
                Command="{Binding AddProductCommand}"
                FontSize="18" />

        <!-- Display Error Message -->
        <Label Text="{Binding ErrorMessage}"
               TextColor="Red"
               FontSize="14"
               IsVisible="{Binding ErrorMessage, Converter={StaticResource NullToBoolConverter}}" />
    </StackLayout>
</ContentPage>

Using Alerts for Errors

private async void OnAddProduct()
{
    try
    {
        if (string.IsNullOrWhiteSpace(NewProductName))
            throw new ArgumentException("Product name cannot be empty.");

        Products.Add(new Product { Name = NewProductName });
        NewProductName = string.Empty;
    }
    catch (Exception ex)
    {
        await Application.Current.MainPage.DisplayAlert("Error", ex.Message, "OK");
    }
}
,