Unexpected Type Error on Text.setText()

I see a few occurrences of a weird error in the ERA related to the onSettingsChanged() method in datafield. All the supported by my app devices with different firmware versions and languages are affected. However, I was never able to reproduce it on my own Edge device. Here is a shortened version of the code where it happens:

import MonkeyMath;
import Toybox.Activity;
import Toybox.Application;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
import Toybox.UserProfile;
import Toybox.WatchUi;

public class Power extends Component {
    private var _ftp as Number;
    private var _powerZoneLimits as Array<Number>;
    private var _powerContainer as RollingAverage?;
    private var _powerLabelDrawable as Text?;

    public function initialize() {
        Component.initialize();
        _ftp = getNumberProperty("ftp");
        _powerZoneLimits = computePowerZoneLimits();
        var powerAvgSeconds = getNumberProperty("powerAvgSeconds");
        _powerContainer = powerAvgSeconds > 1 ? new RollingAverage(powerAvgSeconds) : null;
    }

    public function compute(info as Info) as Void {
    ...
    }

    public function onLayout(dc as Dc, view as View) as Void {
        _powerLabelDrawable = view.findDrawableById("powerLabel");
        var powerLabelText = Lang.format(Application.loadResource(Rez.Strings.power), [
            getNumberProperty("powerAvgSeconds"),
        ]);
        _powerLabelDrawable.setText(powerLabelText);
    }

    public function onUpdate(dc as Dc, textColor as ColorType) as Void {
    ...
    }

    public function onSettingsChanged() as Void {
        _ftp = getNumberProperty("ftp");
        _powerZoneLimits = computePowerZoneLimits();
        var powerAvgSeconds = getNumberProperty("powerAvgSeconds");
        _powerContainer = powerAvgSeconds > 1 ? new RollingAverage(powerAvgSeconds) : null;
        var powerLabelText = Lang.format(Application.loadResource(Rez.Strings.power), [powerAvgSeconds]);
        _powerLabelDrawable.setText(powerLabelText); // The Unexpected Type Error arises here according to ERA
    }
}

So it points me to the Text.setText() method. But how is it even possible?

Even if we assume that ERA points to the culprit code row not precisely, I don't see the problem in a few previous rows as well.

For the lines 41, 43 the getNumberProperty() method looks like below:

public function getNumberProperty(propertyKey as String) as Number {
    try {
        var value = Properties.getValue(propertyKey).toNumber();
        return value != null ? value : SETTINGS_DEFAULTS.get(propertyKey);
    } catch (e) {
        return SETTINGS_DEFAULTS.get(propertyKey);
    }
}

public static const SETTINGS_DEFAULTS =
    {
        "ftp" => 200,
        "powerAvgSeconds" => 3,
    } as Dictionary<String, Number>;

I think it covers any "unhappy" scenario during the property retrieval:

  1. If Properties.getValue() fails internally then the catch block will return the default prop value from the constant dictionary;
  2. If Properties.getValue() returns null then the NPE will be thrown and the catch block will return the default prop value from the constant dictionary;
  3. If Properties.getValue() returns a String that is not parsable to Number then, according to the documentation, toNumber() will return null and the default prop value will be taken from the constant dictionary;
  4. If the Properties.getValue() returns a parsable String then it will be successfully cast to Number (the behavior that we usually see when changing settings from Android);
  5. If Properties.getValue() returns a Number then it will be successfully returned as is after the toNumber() invocation.

Line 42 cannot be a culprit as well, or in another case, the ERA would show a deeper stack trace.

Line 44 looks also fine - all types seem to be correct and, if something were wrong inside the new RollingAverage() constructor, then the stack trace would point inside the constructor.

Line 45 - simple Lang.format() call with 2 non-null arguments.

Line 46 - simple Text.setText() call with String argument since Lang.format() always returns a String.

My simplified properties file looks like this:

<properties>
    <property id="ftp" type="number">200</property>
    <property id="powerAvgSeconds" type="number">3</property>
</properties>

The simplified settings file:

<settings>
    <setting propertyKey="@Properties.ftp" title="@Strings.ftp">
        <settingConfig type="numeric" min="50" max="1000"/>
    </setting>
    <setting propertyKey="@Properties.powerAvgSeconds" title="@Strings.powerAvgSeconds">
        <settingConfig type="numeric" min="1"/>
    </setting>
</settings>

It defines that the type of settings are numeric and that they may have some min and max values.

The simplified strings file:

<strings>
    <!-- settings labels -->
    <string id="ftp" scope="settings">Functional threshold power</string>
    <string id="powerAvgSeconds" scope="settings">Number of seconds to average current power</string>

    <!-- display labels -->
    <string id="power">Power $1$s</string>
</strings>

I'm totally lost in assumptions about what may be wrong here and how to reproduce the problem.

  • I think they might be changing the settings (from Connect IQ) while the app is in some strange state that does run code but has no layout? Just a guess

  • Nice assumption! I just tried a few options to change these settings on my device when datafield UI is invisible:

    • when the app is running but another datascreen is selected;
    • when the app is running in "background" mode after quitting to the Edge home screen;
    • when the app is stopped due to the switching to another activity profile;
    • when the app is stopped due to removing it from the datascreen.

    But in all these cases a new setting value was applied successfully. So still have no clue how the settings may be changed in a way that causes the app to crash

  • Yes, it is applied, and then the onSettingsChanged might be called (probably depending on the scenario). My guess is that somehow it is called in one of the scenarios when there is no onLayout (i.e background process). If this is the case you might even not see the crash. Maybe try this: add a print at the beginning and at the end of onSettingsChanged. See if it's called and if it manages to get to the end.

    Another thing you could do, is to be on the safe side and check for _powerLabelDrawable != null

  • Maybe try this: add a print at the beginning and at the end of onSettingsChanged.

    It would be cool and very helpful if we could read such "log.debug" messages in ERA, but in sim it always manages to execute a full method. And in my own device too, since I don't see any similar errors in on-device log files.

    However, it looks you are right about the null check. If I explicitly assign null to the _powerLabelDrawable

    _powerLabelDrawable = null as Text?;
    _powerLabelDrawable.setText(powerLabelText);

    I'm getting the same error in the log console:

    Error: Unexpected Type Error
    Details: Failed invoking <symbol>

    So yeah, looks like null check will be a nice addition to prevent the app crash. Thank you!

  • If Properties.getValue() returns null then the NPE will be thrown and the catch block will return the default prop value from the constant dictionary;

    Yesterday I discovered that a catch block cannot handle NPE and this results in an Unexpected Type Error with a subsequent app crash. Writing code that may throw NPE is for sure a smell from the dev side but what a useless construction this catch block in Monkey C! Instead of a simple catching in one place of everything that may be wrong during the Properties.getValue() call I have to guess myself every place where IQ API may unexpectedly return null because of its internal bugs

  • yeah, not great. At least you can prevent an NPE by checking the value before using it, but in other cases it's even worse