---
title: .NET MAUI Crash Reporting and Error Tracking
description: Set up Error Tracking for your .NET MAUI applications.
breadcrumbs: >-
  Docs > RUM & Session Replay > Application Monitoring > .NET MAUI Monitoring >
  .NET MAUI Crash Reporting and Error Tracking
---

> For the complete documentation index, see [llms.txt](https://docs.datadoghq.com/llms.txt).

# .NET MAUI Crash Reporting and Error Tracking

## Overview{% #overview %}

Error Tracking processes errors collected from the .NET MAUI SDK.

Enable .NET MAUI Crash Reporting and Error Tracking to get comprehensive crash reports, symbolicated C# and native iOS stack traces, deobfuscated Android stack traces, and error trends across iOS and Android. Your crash reports appear in [Error Tracking](https://app.datadoghq.com/rum/error-tracking).

### C# error tracking{% #c-error-tracking %}

C# error tracking is enabled automatically as soon as RUM is enabled — no extra configuration is required. The SDK captures:

- Unhandled C# exceptions (`AppDomain.UnhandledException`)
- Unobserved task exceptions (`TaskScheduler.UnobservedTaskException`)

You can also manually report an error with `DdRum.AddError`.

### Native crash reporting (optional){% #native-crash-reporting-optional %}

`NativeCrashReportEnabled` is **only** needed if you also want to capture crashes that originate from native iOS or Android code — for example, an Objective-C/Swift crash on iOS, or a JNI/Kotlin crash on Android. C# error tracking works without it.

To enable native crash reporting, set `NativeCrashReportEnabled = true` on the SDK configuration:

```csharp
.UseDatadog(new DdSdkConfiguration
{
    ClientToken = "<CLIENT_TOKEN>",
    Environment = "<ENV_NAME>",
    TrackingConsent = TrackingConsent.Granted,
    NativeCrashReportEnabled = true,
})
```

{% alert level="info" %}
When `NativeCrashReportEnabled = true`, an unhandled C# exception that crashes an iOS app on the main thread is reported **twice**. One report is a C# error captured by `AppDomain.UnhandledException`. The other is a native crash captured by the iOS crash reporter. Both events share the same RUM session and view, so you can correlate them in the Explorer.  On Android, the C# crash is reported once, although a separate native crash can sometimes appear while the app shuts down.  To keep only one of the two, use [`ErrorEventMapper`](https://docs.datadoghq.com/real_user_monitoring/application_monitoring/maui/advanced_configuration.md#modify-or-drop-rum-events) to drop the copy that doesn't fit your workflow. For example, filter by `Source` or `Stacktrace` content.
{% /alert %}

## Setup{% #setup %}

If you have not set up the .NET MAUI SDK yet, follow the [in-app setup instructions](https://app.datadoghq.com/rum/application/create) or see the [.NET MAUI setup documentation](https://docs.datadoghq.com/real_user_monitoring/application_monitoring/maui/setup.md).

## Get symbolicated stack traces{% #get-symbolicated-stack-traces %}

To resolve C# file names and line numbers, native iOS crash addresses, and obfuscated Android class and method names, upload your app's symbol files to Datadog. Symbolication then happens server side on each error event.

| Platform        | Symbol file           | What it covers                                            | Generated by                                                |
| --------------- | --------------------- | --------------------------------------------------------- | ----------------------------------------------------------- |
| iOS             | `.dSYM` bundle        | Native crashes and AOT-compiled C# method names           | `dotnet publish -r ios-arm64`                               |
| Android         | `mapping.txt`         | R8/ProGuard obfuscated Java/Kotlin class and method names | `dotnet publish` with R8 enabled (see Enable R8 on Android) |
| iOS and Android | Portable PDB (`.pdb`) | File names and line numbers for managed C# stack traces   | `dotnet publish` with portable debug symbols enabled        |

### How C# symbolication works{% #how-c-symbolication-works %}

.NET MAUI Release builds don't include the files the runtime on the device needs to map a stack frame back to a source file and line. Instead, the SDK resolves C# frames server-side using your app's Portable PDB files:

1. At build time, the `Datadog.Maui` package generates a debug ID manifest (`dd_debug_ids.json`) that records the debug ID of each assembly your app ships, and packages it into the app.
1. At runtime, when the SDK reports a C# error, it attaches the assembly debug ID, method token, and IL offset of each managed stack frame.
1. At publish time, the SDK uploads the Portable PDB that matches each shipped assembly. On iOS device builds, the linker rewrites the app's assembly, so the SDK uploads the PDB produced by the linker instead of the pre-link copy in the build output directory.
1. Datadog matches each frame to its Portable PDB by debug ID and resolves the file name and line number.

On iOS, line numbers depend on the IL offsets that the Mono AOT runtime reports. See C# line numbers on iOS.

Portable PDB files do not need to be bundled inside the app. On iOS, when symbol upload is enabled, the SDK removes any Portable PDB from the published app bundle by default (see `DatadogKeepPortablePdbs` in Configuration).

### Upload symbols with `datadog-ci`{% #upload-symbols-with-datadog-ci %}

The `Datadog.Maui` NuGet package ships MSBuild targets that upload symbols to Datadog automatically as part of `dotnet publish`. To enable it:

#### 1. Install `datadog-ci`

```bash
npm install -g @datadog/datadog-ci
```

Verify the installation with `datadog-ci version`. Portable PDB upload requires `datadog-ci` v5.25.0 or later. Earlier versions do not include the `ppdb-symbols upload` command.

#### 2. Set your Datadog API key

Export the key in the shell that runs `dotnet publish`:

```bash
export DATADOG_API_KEY=<YOUR_DATADOG_API_KEY>
```

For CI environments, set `DATADOG_API_KEY` as a protected/secret environment variable on your runner. Do not commit the key to source control.

For local testing, you can pass the key as an MSBuild property (`-p:DatadogApiKey=...`), but **do not set `DatadogApiKey` in your `.csproj`**. That file is checked in to source control.

#### 3. Enable the upload

Set `DatadogUploadSymbols=true` either as a `<PropertyGroup>` entry in your `.csproj` or on the `dotnet publish` command line. The MSBuild targets run automatically after publish. If `datadog-ci` is missing or the API key is unset, the upload is skipped with a message and the build continues.

{% tab title="iOS" %}

```bash
dotnet publish -c Release -f net10.0-ios -r ios-arm64 \
  -p:DatadogUploadSymbols=true
```

The `.dSYM` bundle generated next to the `.app` and the Portable PDBs of your app's assemblies are uploaded. dSYMs are only produced for device builds (`-r ios-arm64`). `dotnet publish` does not support simulator builds (`iossimulator-arm64`), so nothing is uploaded for them, including Portable PDBs. To upload Portable PDBs from a simulator build, run:

```bash
dotnet build -c Release -f net10.0-ios -r iossimulator-arm64 \
  -p:DatadogUploadSymbols=true "-t:Build;DatadogUploadPortablePdb"
```

{% /tab %}

{% tab title="Android" %}

```bash
dotnet publish -c Release -f net10.0-android \
  -p:DatadogUploadSymbols=true
```

The R8 `mapping.txt` file and the Portable PDBs of your app's assemblies are uploaded. The mapping file is only generated when R8 is enabled (see Enable R8 on Android).
{% /tab %}

Portable PDBs are uploaded for your app's own assemblies and project references, meaning every assembly whose `.pdb` file is written to the build output directory. To include PDBs shipped inside NuGet packages, copy them to the build output directory with `<CopyDebugSymbolFilesFromPackages>true</CopyDebugSymbolFilesFromPackages>`. The SDK uploads only the PDBs that match the assemblies your app ships. On Android, assemblies that the trimmer rewrites, such as `Microsoft.Maui`, have no matching PDB, so their frames show method names only.

Portable PDB upload requires portable debug symbols, which is the .NET default. If your project overrides it, set `<DebugType>portable</DebugType>` for the configuration you publish.

Upload the symbols from the same build that you ship. Builds can get new debug IDs even when the source code doesn't change, and iOS Release builds always do, so symbols from another build of the same code don't match.

### Enable R8 on Android{% #enable-r8-on-android %}

By default, .NET MAUI Android Release builds do not run R8, so no `mapping.txt` file is generated. To enable it, add the following to your `.csproj`:

```xml
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <AndroidLinkTool>r8</AndroidLinkTool>
  <AndroidCreateProguardMappingFile>true</AndroidCreateProguardMappingFile>
</PropertyGroup>
```

The mapping file is generated at `bin/Release/net10.0-android/mapping.txt`.

The mapping file is uploaded with the `DatadogServiceName` property and your app's `ApplicationDisplayVersion`. For Datadog to match it with your crash reports, both values must match the service and version that the SDK reports at runtime.

### Configuration{% #configuration %}

All configuration is done through MSBuild properties, either in your `.csproj` `<PropertyGroup>` or on the `dotnet publish` command line with `-p:`.

| Property                         | Required | Default           | Description                                                                                                                                                                                                                                                                                                                            |
| -------------------------------- | -------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DatadogUploadSymbols`           | Yes      | `false`           | Set to `true` to enable symbol upload after publish.                                                                                                                                                                                                                                                                                   |
| `DatadogServiceName`             | No       | `$(AssemblyName)` | Service name used to identify your app in Datadog. Must match the `Service` you pass to `DdSdkConfiguration` at runtime.                                                                                                                                                                                                               |
| `DatadogSite`                    | No       | —                 | The Datadog site that receives the upload (for example, `datadoghq.eu`, `us5.datadoghq.com`). Must match the `Site` value set on `DdSdkConfiguration`. If unset, `datadog-ci` reads the `DATADOG_SITE` or `DD_SITE` environment variable, and defaults to `datadoghq.com`. Setting this property overrides both environment variables. |
| `DatadogApiKey`                  | No       | —                 | API key passed directly. If unset, the `DATADOG_API_KEY` environment variable is used instead.                                                                                                                                                                                                                                         |
| `DatadogFailOnSymbolUploadError` | No       | `false`           | Set to `true` to fail the build when a symbol upload fails. By default, failures are reported as MSBuild warnings.                                                                                                                                                                                                                     |
| `DatadogKeepPortablePdbs`        | No       | `false`           | iOS only. Applies only when `DatadogUploadSymbols=true`. Some debug configurations (for example, `soft-debug`) bundle the Portable PDB inside the `.app`. Because C# symbolication uses the uploaded PDB, the SDK removes it from the bundle by default. Set to `true` to keep it in the bundle.                                       |

```xml
<PropertyGroup>
  <DatadogServiceName>my-maui-app</DatadogServiceName>
  <DatadogSite>datadoghq.eu</DatadogSite>
</PropertyGroup>
```

The terminal logger that `dotnet publish` uses by default hides informational output. To see the Datadog upload messages, add `-v n -tl:off`:

```bash
dotnet publish -c Release -f net10.0-ios -r ios-arm64 \
  -p:DatadogUploadSymbols=true -v n -tl:off
```

After publish, a summary shows the outcome of each upload (`uploaded`, `FAILED`, `skipped`, or `not run`).

### Upload failures{% #upload-failures %}

By default, a failed upload produces an MSBuild warning and the build still succeeds, so a symbol upload problem does not block shipping your app. To turn upload failures into build errors, which is recommended in CI, pass `-p:DatadogFailOnSymbolUploadError=true`.

After upload, symbols take up to 5 minutes to process. You can confirm they were received under [Error Tracking > Settings > Symbol Files](https://app.datadoghq.com/source-code/setup/symbols).

### Upload Portable PDBs manually{% #upload-portable-pdbs-manually %}

Datadog recommends using the MSBuild targets above, because they pick the right PDB files for you. If you need to upload from a separate step, run:

```bash
datadog-ci ppdb-symbols upload <FOLDER_WITH_PDBS> \
  --debug-id-manifest obj/<CONFIGURATION>/<TARGET_FRAMEWORK>/<RUNTIME_IDENTIFIER>/dd_debug_ids.json
```

On iOS device builds, upload the PDB files from `obj/.../linked/`, not from `bin/`. The linker rewrites your assemblies, and only the PDBs it writes match the app you ship. Uploading the wrong PDB produces wrong line numbers without any warning.

## Limitations{% #limitations %}

### C# assemblies without Portable PDBs{% #c-assemblies-without-portable-pdbs %}

C# frames resolve to file names and line numbers only for assemblies whose Portable PDB was uploaded. Frames from .NET framework assemblies, from assemblies that the Android trimmer rewrites (such as `Microsoft.Maui`), and from third-party assemblies whose PDB is not in the build output directory, resolve to method names only.

### Manually reported errors{% #manually-reported-errors %}

C# file names and line numbers are resolved only for errors the SDK captures automatically: unhandled exceptions and unobserved task exceptions. Errors you report with `DdRum.AddError` pass the stack trace as text, so they are shown as reported, without file names or line numbers.

### C# errors in Log Management{% #c-errors-in-log-management %}

C# symbolication applies to errors collected by RUM. C# stack traces sent to Log Management are not symbolicated.

### C# line numbers on iOS{% #c-line-numbers-on-ios %}

On iOS, Release builds report C# frames without an IL offset. For those frames, Datadog shows the file name and the first line of the method, not the exact line that threw the exception.

To get exact line numbers, compile the app with debug information (soft-debug) by publishing with both of the following properties:

```bash
dotnet publish -c Release -f net10.0-ios -r ios-arm64 \
  -p:DatadogUploadSymbols=true -p:MtouchDebug=true -p:MtouchUseLlvm=false
```

Both properties are needed: Release builds use LLVM by default, and LLVM turns soft-debug off. Soft-debug makes the app binary larger and can make the app slower. Debug builds use soft-debug by default.

Soft-debug builds also bundle the Portable PDB inside the `.app`. When symbol upload is enabled, the SDK removes it from the bundle by default, because symbolication uses the uploaded PDB. To keep it, set `DatadogKeepPortablePdbs=true`.

On Android, Release builds report IL offsets, so C# frames show the exact line.

### File sizing{% #file-sizing %}

dSYM bundles (iOS) can go up to **2 GB** each.

### Collection{% #collection %}

The SDK handles crash reporting with the following behaviors:

- Crashes can only be detected after the SDK is initialized. Initialize the SDK as early as possible in `MauiProgram.CreateMauiApp`.
- RUM crashes must be attached to a RUM view. If a crash occurs before a view is visible (or after the app has been moved to the background by the user), the crash is muted and isn't reported. To mitigate this, set `TrackBackgroundEvents = true` on `DdRumConfiguration`.
- Only crashes that occur in sampled sessions are kept.

### Android native crashes{% #android-native-crashes %}

When `NativeCrashReportEnabled = true`, the SDK also captures native (C/C++) crashes on Android. The SDK does not upload any `.so` files for these crashes.

In a MAUI app, most native code comes from the .NET runtime (`libmonosgen-2.0.so`, `libmonodroid.so`), from Datadog's own NDK library, and from C# code compiled ahead of time (`libaot-*.dll.so`). No debug information is available for the runtime and Datadog libraries, so their frames are not symbolicated. C# frames inside a native crash don't show file names or line numbers, even if you upload the `libaot-*.dll.so` files.

If you ship your own native C/C++ libraries, upload their unstripped `.so` files with `datadog-ci elf-symbols upload <PATH_TO_SO_DIRECTORY>`.

## Test your implementation{% #test-your-implementation %}

To verify your Crash Reporting and Error Tracking configuration, trigger a crash and confirm that the error appears in Datadog:

1. Publish your application with `DatadogUploadSymbols=true` and run it on a real device or emulator (dSYMs are generated only for device builds on iOS).

1. Execute code that throws an unhandled exception. For example:

   ```csharp
   void OnButtonClicked(object sender, EventArgs e)
   {
       throw new InvalidOperationException("Crash the app");
   }
   ```

1. After the crash, restart your application and wait for the SDK to upload the crash report.

1. Confirm the event in [Error Tracking](https://app.datadoghq.com/rum/error-tracking). C# frames from your app's assemblies show file names and line numbers, native iOS frames from a device build are symbolicated, and obfuscated Android frames are deobfuscated.

## Troubleshooting{% #troubleshooting %}

**`Skipping symbol upload — datadog-ci is not installed`**: Run `npm install -g @datadog/datadog-ci` and verify with `datadog-ci version`.

**`Skipping symbol upload — DATADOG_API_KEY is not set`**: Export the key in your shell: `export DATADOG_API_KEY=<YOUR_DATADOG_API_KEY>`. Verify with `echo $DATADOG_API_KEY`.

**`Skipping dSYM upload — file not found`**: dSYMs are only generated for device builds (`-r ios-arm64`). Simulator builds do not produce dSYMs. To upload Portable PDBs from a simulator build, see the iOS tab in Enable the upload.

**`Skipping mapping upload — mapping.txt not found`**: R8 is not enabled for the build. Add `<AndroidLinkTool>r8</AndroidLinkTool>` and `<AndroidCreateProguardMappingFile>true</AndroidCreateProguardMappingFile>` to your `.csproj`. See Enable R8 on Android.

**`Skipping Portable PDB upload — no debug-id manifest found`**: The debug ID manifest (`dd_debug_ids.json`) is generated during the build that `dotnet publish` runs. Confirm that the build succeeded and that debug symbols are enabled with `<DebugType>portable</DebugType>`.

**`Skipping Portable PDB upload — no Portable PDB found`**: No `.pdb` file was written to the build output directory. Confirm that debug symbols are enabled for the configuration you publish, with `<DebugType>portable</DebugType>`.

**Warning: no Portable PDB matching the shipped assembly was found**: For one or more assemblies, none of the PDB files found matches the assembly the app ships, so C# frames from those assemblies are not resolved to file names and line numbers. The publish output lists each affected assembly in a `not uploading a Portable PDB for <assembly>` line. This can happen when the PDB files are left over from an earlier build. Delete the `bin/` and `obj/` directories and publish again.

**C# frames show method names but no file names or line numbers**: Confirm that the publish summary reports the Portable PDB upload as `uploaded`, and that the frames belong to one of your app's assemblies. See C# assemblies without Portable PDBs. If the error was reported with `DdRum.AddError`, see Manually reported errors.

**iOS C# frames show the first line of the method instead of the line that threw**: iOS Release builds report C# frames without an IL offset. See C# line numbers on iOS.

**No Datadog output visible during publish**: The terminal logger hides informational messages. Add `-v n -tl:off` to your `dotnet publish` command.

**Upload completes but symbols don't appear in Datadog**: Symbols take up to 5 minutes to process. Check [Error Tracking > Settings > Symbol Files](https://app.datadoghq.com/source-code/setup/symbols).

## Further Reading{% #further-reading %}

Additional helpful documentation, links, and articles:

- [Get started with Error Tracking](https://docs.datadoghq.com/real_user_monitoring/error_tracking.md)
- [Visualize Error Tracking data in the Explorer](https://docs.datadoghq.com/real_user_monitoring/error_tracking/explorer.md)
