Global Error Handling for UWP-Apps

[ Für Details zur Fehlerbehandlung in .NET im Allgemeinen, unter Berücksichtigung von von multi-threaded Programmierung (insbesondere mit Task und Await)  siehe meinen Artikel “Lass da mal was sein” im dotnetpro Magazin 3/2013.
Für Details zur Fehlerbehandlung in UWP-Apps siehe meinen Artikel “Was war da los?” im dotnetpro Magazin 12/2016. ]

This post is the result of my experiments with handling unexpected errors in UWP-Apps.
For my approach about tracing see Tracing for UWP-Apps

It is debatable if showing information about unexpected exceptions to users is good idea. I have had Mail, Translator, Calculator, the Store app and other UWP-Apps abort without any feedback on crash or at restart. I personally find it annoying when apps simply disappear without any hint. The “little  Watson” solution of displaying error info on the next app start feels weird to me. Both behaviors doe not conform with common user expectations.

If crash error details can be helpful for end users depends on the app type and the target audience. For enterprise apps I know from long experience that error notifications (even with deep technical infos) are definitely helpful. Users are often able to circumvent problems and keep working with defective apps without even contacting support (while support might have been made aware of the problem automatically in the background…).

Options to inform users about unexpected exceptions are:

  • Never notify users about exceptions.
  • Show dialog in App.UnhandledException
  • Show dialog on next app start.
  • Show exception details like Message and HResult
  • Show “Sorry…” only.

This following code sample implements the main options, configurable via _notifyUserAboutCrash.

Some implementation notes:

  • UnhandledExceptionEventArgs.Exception returns useful values only on first access.
    See Why does UnhandledExceptionEventArgs.Exception return useful info only on first access?
    As a workaround one can assign it to a local variable and use this subsequently
  • Async calls in UnhandledException only work after setting e.Handled=True.
    UnhandledException  does not offer a deferral.
  • One e.Handled has been set to True setting it to False again does not re-enable automatic app abort. Thus the sample re-throws the exception to make the app crash.
  • Under the debugger the sample only works properly with the advanced compile option
    DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION.
    Without this async calls in UnhandledException throw.
  • The sample uses my custom trace class.
  • I am configuring HockeyApp without WindowsCollectors.UnhandledException.
    This disables automatic exception tracking and allows to explicitly use TrackException when desired.

Download complete code.

Exception details

The exception details available via UnhandledExceptionEventArgs are more detailed in debug mode then in release mode. Sometimes e.Message contains more details then e.Exception.Message. In debug mode it contains the start of the StackTrace.

Infois provides by UnhandledExceptionEventArgs

Infos provided by UnhandledExceptionEventArgs

Sample implementation

Enum NotifyUserAboutCrash
    OnError
    OnNextStart
    Never
End Enum
Const _notifyUserAboutCrash As NotifyUserAboutCrash = NotifyUserAboutCrash.OnError

Sub New()
    AddHandler TaskScheduler.UnobservedTaskException, AddressOf TaskScheduler_UnobservedTaskException
    Trace.Init(TraceTarget.Debug Or TraceTarget.BufferedFile, TraceLevel.Debug)

    HockeyClient.Current.Configure(“<your App ID>”,
                                   New TelemetryConfiguration() With {.Collectors = WindowsCollectors.Metadata}).
        SetExceptionDescriptionLoader(
            Function(ex As System.Exception)
                Return GetBaseExceptionMessage(ex)
            End Function)
End Sub

Protected Overrides Async Sub OnLaunched(e As Windows.ApplicationModel.Activation.LaunchActivatedEventArgs)
...
    If _notifyUserAboutCrash = NotifyUserAboutCrash.OnNextStart Then
        Await ProblemReporter.NotifyLastExToUserIfExisting()
    End If
...
End Sub

Private Shared _isAborting As Boolean = False
Private Async Sub App_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
    If _isAborting Then Return
    'Must store e.Exception in local variable because it returns useful values on first access only
    '  Even watching the value in debugger destroys it. Never set a breakpoint before Trace.Debug()!
    Dim unhandledEx = New Exception(e.Message, e.Exception)

    'Without Handled=True async code does crash
    '  which forces us to rethrow the ex to abort the app
    e.Handled = True

    If HandleRecoverableExceptions(unhandledEx.InnerException) Then
        Trace.Debug($"Recovered from exception: {unhandledEx.Message}")
        Await Trace.FlushAsync()
        Return
    End If

    Trace.Error($"Unexpected exception: {unhandledEx.InnerException.ToString}")
    Await Trace.CompleteAsync()
    Try
        ProblemReporter.SaveAsLastException(unhandledEx)
        HockeyClient.Current.TrackException(unhandledEx.InnerException)
        HockeyClient.Current.Flush()
    Catch ex As System.Exception
        Trace.Error(ex)
    End Try

    If _notifyUserAboutCrash = NotifyUserAboutCrash.OnError Then
        Await ProblemReporter.NotifyExceptionToUserViaMessageDialogAsync(unhandledEx)
    End If

    'reenabling auto crash via e.Handled=False does not work
    Await ForceAppCrashAsync(unhandledEx)
End Sub

Private Shared Async Function ForceAppCrashAsync(unhandledEx As Exception) As Task
    Trace.Fatal("!!! aborting")
    Await Trace.CompleteAsync()
    _isAborting = True
    'Rethrow ex with a StackTrace as if it came form the original location
    ExceptionDispatchInfo.Capture(unhandledEx.InnerException).Throw()
End Function

Private Function HandleRecoverableExceptions(ex As System.Exception) as Boolean
    Select Case True
        Case TypeOf (ex) Is MyCustomException
            Return True
            'Case TypeOf (unhandledException) Is ???
        Case Else
            Return False
    End Select
End Function

Private Async Sub TaskScheduler_UnobservedTaskException(sender As Object, e As UnobservedTaskExceptionEventArgs)
    'UnobservedTaskExceptions get tracked by HockeyApp v4.1.1+
    Dim ex = e.Exception
    Trace.Warn($"TaskScheduler_UnobservedTaskException: {ex.ToString}")
    Await Trace.FlushAsync
    HockeyClient.Current.TrackException(ex)
    HockeyClient.Current.Flush()
    e.SetObserved()
End Sub
Error dialog from UnhandledException

Error dialog from UnhandledException

Error dialog on next app start

Error dialog on next app start

About Peter Meinl

IT Consultant
This entry was posted in Computers and Internet and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s