воскресенье, 21 мая 2017 г.

How Unity notifies about innerException: rethrow in stack

Suppose code:

public void TestCodeBelowRxChainExecutedDueExceptionHandled()
        {
            UiLogger.Trace("1");
            Observable.Return(1)
                .Select(a =>
                {
                    throw new InvalidOperationException("aa");
                    return a;
                })
                .Subscribe(
                    Actions.Empty,
                    exception => Debug.LogException(new AssertionFailedException(exception, "mmm assertion failed")); // (*)
            UiLogger.Trace("2"); // not reached if not (*)
        }


Here exception, which is thrown in Select, is logged with Debug.LogException as new AssertionFailedException. This is done to make code below execute anyway.


Application.logMessageReceived receives:

condition: InvalidOperationException: aa
stacktrace:
Game.Impl.Sheets.CrashSheet.<TestCodeBelowRxChainExecutedDueExceptionHandled>m__6 (Int32 a) (at Assets/Game/Impl/Sheets/CrashSheet.cs:210)
UniRx.Operators.SelectObservable`2+Select[System.Int32,System.Int32].OnNext (Int32 value) (at Assets/Plugins/UniRx/Scripts/Operators/Select.cs:84)
Rethrow as AssertionFailedException: mmm assertion failed // (**)
UnityEngine.Debug:LogException(Exception)
Game.Utils.Lc:Assertion(Exception, String) (at Assets/Game/Utils/Lc.cs:14)
Game.Impl.Sheets.CrashSheet:<TestCodeBelowRxChainExecutedDueExceptionHandled>m__7(Exception) (at Assets/Game/Impl/Sheets/CrashSheet.cs:215)
UniRx.Subscribe`1:OnError(Exception) (at Assets/Plugins/UniRx/Scripts/Observer.cs:172)
UniRx.Operators.Select:OnNext(Int32) (at Assets/Plugins/UniRx/Scripts/Operators/Select.cs:88)
UniRx.Operators.ImmediateReturnObservable`1:Subscribe(IObserver`1) (at Assets/Plugins/UniRx/Scripts/Operators/Return.cs:87)
UniRx.Operators.SelectObservable`2:SubscribeCore(IObserver`1, IDisposable) (at Assets/Plugins/UniRx/Scripts/Operators/Select.cs:61)
UniRx.Operators.OperatorObservableBase`1:Subscribe(IObserver`1) (at Assets/Plugins/UniRx/Scripts/Operators/OperatorObservableBase.cs:34)
UniRx.ObservableExtensions:Subscribe(IObservable`1, Action`1, Action`1) (at Assets/Plugins/UniRx/Scripts/Observer.cs:418)
Game.Impl.Sheets.CrashSheet:TestCodeBelowRxChainExecutedDueExceptionHandled() (at Assets/Game/Impl/Sheets/CrashSheet.cs:213)
DeepTest.Components.MainOnScene:Start() (at Assets/Game/InternalApi/MainOnScene.cs:14)

type: Exception

Note: see (**) "Rethrow as <Exception message>", so Unity's Debug.LogException takes care about Exception.innerException field. It adds exception messages right into stack. So, stack is constructed the following way:


throw site's stack <-- for example, dictionary[non-existent key] throws KeyNotFoundException
[rethrow + message of new exception(original)] <-- that is how client code handles it, e.g. throw NetworkResponseValidationException("wrong response", request)
stack of new wrapping exception

In that case, I think NetworkResponseValidationException.StackTrace gives us better understanding what is happenning, than stacktrace of dictionary calls. When you will be fixing such exception, the most common is to submit a bug to server or fix client response schema. You wount fix code of Dictionary. So, stack of innerException is odd one here. And grouping alike exceptions (usual is by first line of stacktrace) gives us something like:


KeyNotFoundException at dictionary

And only when you open group of such exceptions, you'll see that they are coming one from MakePurchaseRequest, another from TrackSignInAnalyticsEventRequest. It's different exceptions which are grouped together. That's confusing.

In conclusion, I think, we must see stacktrace of wrapping exception and all the inner exceptions can be logged as additional info if needed.

And a few words about assertions.

What is assertions?

In my practice, we don't write unit tests in regular understanding. What the program must do when something is wrong? Crash, that's it. But it's comfortable only for us, programmers. Business asks for programs which continue running when some picture failed to load from internet or database does not contain some data, etc. So, we return to initial state or do some default behaviour in case of exception and make an assertion call. Assertion is logged (e.g. to crashlytics) in release build (so we can see and fix it ASAP) and thrown in debug. So one programmer can say to others to not forget to implement some switch case when extending his/her code:


switch (state)
{
  case READY:
    view.SetReady();
    break();
  case RUNNING:
    view.SetRunning();
    break;
  default:
    Debug.Assertion("state not implemented: " + state);
}

When dealing with AssertionFailedException, you allways need stacktrace of the place, where assertion occured. So I prefer log assertions that way:


Debug.LogException(new AssertionFailedException(<message>))

If we make an assertion because we receive an exception, we just make it inner:


try
{
  MakePurchase(productId);
}
catch (ProductNotAvailableException exception)
{
  Debug.LogException(new AssertionFailedException("tell the administator to see what's wrong with productId requested by client: " + productId, exception));
}

So, stacktrace shows us to catch site which is good and we have a parameter in crashlytics, containing inner exception.

Note: by the way, crashlytics, when parse stack with "rethrow..." just skips that line.

Комментариев нет:

Отправить комментарий