JSON loading causes WebGL build index out of range exceptions

Hi, my game has question and response text which appears at various points, which is all contained in one JSON file.

The game works in the Unity editor with no problems, and all the text appears in the right place and time.

But in the WebGL build, the first question text does appear, ie it must have loaded this correctly from the JSON file at least, but after correct answer is given, no response text appears at all, and the game is then stuck.
This is a 256MB memory size development build, with full stack trace, and it produces error exact same place in Chrome or Edge browser in local host.

I’m guessing there’s something wrong with JSON loading code.
EDIT
After @Bunny83 's very helpful comments, below is revised code for LoadJSONData.cs, the main file which loads the JSON data. However, error still remains, now with socket error also as my comment below:

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class LoadJSONData : MonoBehaviour
{
    private string filePath;
    private string jsonData = "";
    [HideInInspector]
    public JSONquestions jsonQuestions;
    IEnumerator LoadJSON()
    {
        if (filePath.Contains("://") || filePath.Contains(":///"))
        {
            UnityWebRequest www = UnityWebRequest.Get(filePath);
            yield return www.SendWebRequest();

            if (www.isHttpError || www.isNetworkError)
            {
                Debug.LogError("There was an HTTP or network error trying to request the URI of the JSON file.");
                yield return null;
            }

            jsonData = www.downloadHandler.text;
            if (jsonData != "" && jsonData != null)
            {
                jsonQuestions = JsonUtility.FromJson<GameData>(jsonData).jsonQuestions;
                Debug.Log("1: Just ran jsonQuestions line in LoadJSON, jsonQuestions is " + jsonQuestions + "

And file path is " + filePath + "
");
}
else { Debug.LogError(“2: JSON file is empty for some reason! jsonData is: " + jsonData + " exactly that my friend.”);
}
}
else if (System.IO.File.Exists(filePath)) {
jsonData = System.IO.File.ReadAllText(filePath);
if (jsonData != “” && jsonData != null)
{
jsonQuestions = JsonUtility.FromJson(jsonData).jsonQuestions;
}
else { Debug.LogError(“3: In LoadJSONData.cs, JSON file is empty for some reason!”); }
yield return null;
} else { Debug.LogError(“4: In LoadJSONData.cs, Filepath doesnt exist it seems, so JSON file not loaded.”);
yield return null;
}

    }

    private void Awake()
    {
        filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "DungeonQuestions.JSON");
        StartCoroutine(LoadJSON());    }
}

[System.Serializable]
public class GameData
{
    public JSONquestions jsonQuestions;
}
[System.Serializable]
public class JSONquestions
{
    public Qref Mary;
    public Qref Farmgirl;
    public Qref BatType;
    public Qref JoansName;
    public Qref TowerPlace;
    public Qref CallLift;
    public Qref EscalierButton;
    public Qref Sortie;
}

[System.Serializable]
public class Qref
{
    public QuestionText Qtext;
    public QuestionText Ctext;
    public QuestionText W1text;
    public QuestionText W2text;
}

[System.Serializable]
public class QuestionText
{
    public string[] phrase;
    public float[] delay;
    public QuestionText(string[] Phrase, float[] Delay)
    {
        this.phrase = Phrase; this.delay = Delay;
    }
  public QuestionText DeepCopy()
    {
        // check for array size separately
        int p = this.phrase.Length;
        int d = this.delay.Length;
        //iterate over p and d
        string[] phraseCopy = new string[p];
        float[] delayCopy = new float[d];
        for (int i = 0; i < p; i++)
        {
            phraseCopy _= this.phrase*;*_

}
for (int j = 0; j < d; j++)
{
delayCopy[j] = this.delay[j];
}
QuestionText copy = new QuestionText(phraseCopy, delayCopy);
return copy;
}
}
The DeepCopy() methods above are designed to substitute player’s input words within JSON file phrases, without altering the original text in JSON file.
EDIT2
Here are new error traces before game freezes, method called MarySuccess() which does contain a call to DeepCopy() within this method, but now not clear if DeepCopy() relevant:
blob:http://127.0.0.…9-67cd60563024:8758 IndexOutOfRangeException: Array index is out of range.
at Questions+c__Iterator0.MoveNext () [0x00000] in :0
at UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) [0x00000] in :0
at UnityEngine.MonoBehaviour.StartCoroutineManaged2 (IEnumerator enumerator) [0x00000] in :0
at UnityEngine.MonoBehaviour.StartCoroutine (IEnumerator routine) [0x00000] in :0
at Questions.MarySet () [0x00000] in :0
at Questions+SetDelegate.Invoke () [0x00000] in :0
at Questions.SetQuestion (Thoughts panel, .SetDelegate setDelegate) [0x00000] in :0
at PriestController3D+c__Iterator0.MoveNext () [0x00000] in :0
at UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) [0x00000] in :0
UnityEngine.MonoBehaviour:StartCoroutineManaged2(IEnumerator)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
Questions:MarySet()
SetDelegate:Invoke()
Questions:SetQuestion(Thoughts, SetDelegate)
c__Iterator0:MoveNext()
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)

(Filename: currently not available on il2cpp Line: -1)
blob:http://127.0.0.…-67cd60563024:13147 WebSocket connection to ‘ws://192.168.1.67:54998/’ failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

blob:http://127.0.0.…9-67cd60563024:8758 IndexOutOfRangeException: Array index is out of range.
at Questions.MarySuccess (System.String inputText) [0x00000] in :0
at Questions+ResponseDelegate.Invoke (System.String input) [0x00000] in :0
at Questions.ProcessAnswer (UnityEngine.UI.InputField input, System.Collections.Generic.List1 answers, Single clockStart, .ResponseDelegate gotItRight, .ResponseDelegate gotItWrong) [0x00000] in <filename unknown>:0*_ <em>*at PriestController3D+<Floorbound>c__Iterator0.<>m__0 (System.String ) [0x00000] in <filename unknown>:0*</em> _*at UnityEngine.Events.UnityAction1[System.Object].Invoke (System.Object arg0) [0x00000] in :0
at UnityEngine.Events.InvokableCall1[System.Object].Invoke (System.Object args0) [0x00000] in <filename unknown>:0*_ _*at UnityEngine.Events.UnityEvent1[System.Object].Invoke (System.Object arg0) [0x00000] in :0
at UnityEngine.UI.InputField.SendOnSubmit () [0x00000] in :0
at UnityEngine.UI.InputField.DeactivateInputField () [0x00000] in :0
at UnityEngine.UI.InputField.OnUpdateSelected (UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in :0
at UnityEngine.EventSystems.ExecuteEvents.Execute (IUpdateSelectedHandler handler, UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in :0
at UnityEngine.EventSystems.ExecuteEvents+EventFunction1[System.Object].Invoke (System.Object handler, UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in <filename unknown>:0*_ _*at UnityEngine.EventSystems.ExecuteEvents.Execute[Object] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.EventFunction1 functor) [0x00000] in :0
at UnityEngine.EventSystems.StandaloneInputModule.SendUpdateEventToSelectedObject () [0x00000] in :0
at UnityEngine.EventSystems.StandaloneInputModule.Process () [0x00000] in :0
at UnityEngine.EventSystems.EventSystem.Update () [0x00000] in :0
UnityEngine.DebugLogHandler:Internal_LogException(Exception, Object)
UnityEngine.DebugLogHandler:LogException(Exception, Object)
UnityEngine.Logger:LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception)
UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
UnityEngine.EventSystems.StandaloneInputModule:SendUpdateEventToSelectedObject()
UnityEngine.EventSystems.StandaloneInputModule:Process()
UnityEngine.EventSystems.EventSystem:Update()

(Filename: currently not available on il2cpp Line: -1)
MarySuccess() method is here, which shows in the last stack trace above before it freezes:
public void MarySuccess(string inputText = “”)
{
player.maryEvent.RemoveAllListeners();
QuestionText C = jsonquestions.Mary.Ctext;
QuestionText Ccopy = C.DeepCopy();
thoughtText.color = new Color(0.55f, 0f, 0.03f); // now matches (141,0,8) colour of input text, was Color.red;
thoughtText.fontSize = 22; // default I put is 28, this text too big to fit
Ccopy.phrase[1] = Ccopy.phrase[1].Replace(“with seconds”, “with " + countdownTimer.TenthSecondsLeft() + " seconds”).Replace(“received bountiful”, “received " + points + " bountiful”);
fillThoughtBox = FillThoughtBox(Thoughts.player, Ccopy.phrase, Ccopy.delay);
StartCoroutine(fillThoughtBox);
StartCoroutine(player.MaryHailed());
}
Thanks any suggestions!

Your first error is in a coroutine you haven’t shown. It should be somewhere in “FillThoughtBox” inside the “Questions” class. The second error is inside “DeepCopy” of your “CorrectText” class.

You have hardcoded a count of “4”. However since you get an index out of bounds exception at least one of those two arrays have less than 4 elements. Are you 100% sure that your arrays always have 4 (or more) elements? If the data is loaded external, from user data or just serialized somewhere else you can never be sure that’s the case. You should practise more defensive programming

Keep in mind that your WrongText1 and WrongText2 classes have the same issue. You usually would check (or use) the Length of the arrays before working with the arrays.

Finally there seems to be no difference between CorrectText, WrongText1 and WrongText2. This is generally a bad design. You have redundant classes which all do exactly the same and hold exactly the same (kind) of data. Even “QuestionText” is the same. You probably should get rid of the “CorrectText, WrongText1 and WrongText2” classes and just use the “QuestionText” class. Your deep copy method can be implemented in that class (however with proper range checking).

EDIT In the end, the problem was JSON had not finished loading asynchronously when referenced from another class, see separate question https://answers.unity.com/questions/1565937/webgl-json-file-loads-but-cant-reference-from-diff.html. I also mentioned somewhere here I thought part of JSON data did show in the first question. This wasn’t true, this text was set in the Unity Editor Inspector.

These are incredibly helpful comments thanks and much more detail than I was expecting. I read stack traces too fast as not used to it and in rush, yes first error is in FillThoughtBox() coroutine I see.

I will do best to implement all your suggestions asap, condense the identical methods and check array size first in more defensive manner. This is first game I made in Unity, as soon as you said the three classes are same, I see this, was trying do too many things first time didn’t even look properly at this bit, in order to get something up to show on my github account to be frank.

I’m still bit confused why this works in Unity Editor but not in WebGL, none of these index out of range errors appear when it runs in Unity Editor. So I was thinking it was something to do with async loading of JSON, that not all of JSON in the file had loaded yet, it’s all very new to me so guessing quite a lot here, after reading Unity JSON intros in different places.

Though I see your point that hard-coding array size is dangerous and will correct, I think in this particular JSON question referenced here, there seem to indeed be 4 elements in each part of array (which I assume is why it doesnt give outofrange error in Unity Editor?).
DeepCopy() method in last error is called by MarySuccess() as trace shows. These are the lines calling it:

CorrectText C = jsonquestions.Mary.Ctext;
CorrectText Ccopy = C.DeepCopy();

and this is ‘Mary.Ctext’ JSON content in the JSON file:

  "Ctext": {
    "phrase": ["Yes, grace indeed, I am sorry good lady, I'm sure you didn't eat too many doughuts!

",
"They won’t be invented for a couple of centuries anyway!
Anyway, with seconds left, I have received bountiful blessings from the Almighty!
",
"Good old Wikipedius!
Where would we be without your thousands of learned scribes, scratching diligently with their quills for no reward other than self-satisfaction?
",
"Oh, Winds Across Sunny Days, I feel these initials will move me now! I feel nervous about looking around here, maybe I can in a sneaky, Mousy kind of manner…
But I must shift my eye to clear my thoughts, focus on leaving this place. Aye aye, I must shift my eye…

"],
“delay”: [3, 3, 5, 0]
}

ie it looks like ‘phrase’ and ‘delay’ arrays above do have 4 elements in them.

The other thing in WebGL game here is that when the first question appears in UI panel, there is no delay between each part of the question, it all displays at once. But when the game is run in Unity Editor there is a delay between each part of question appearing, as there should be from this ‘delay’ array of float values which are delay times set in FillThoughtBox() coroutine below.

I expect you’re far too busy, but in case anyone interested, FillThoughtBox() coroutine here:

public IEnumerator FillThoughtBox(Thoughts panel, string[] phrase, float[] delay, Text objectText = null)
{
    // set text for whether using player thought panel (own thoughts), or third person panel (other creature's voice or thoughts), or an object in the game, 
    // if text to appear on game object, pass the Text component of this object's panel in this case
    if (panel == Thoughts.player) { thoughtText = thoughtButtonPanel.GetComponentInChildren<Text>(); }
    else if (panel == Thoughts.other) { thoughtText = thirdpersonPanel.GetComponentInChildren<Text>(); }
    else if (objectText) { thoughtText = objectText; }
if (thoughtText)
{
    thoughtText.text = phrase[0];
    yield return new WaitForSeconds(delay[0]);
    if (phrase[1] != "")
    {
        thoughtText.text += phrase[1];
        yield return new WaitForSeconds(delay[1]);
    }
    if (phrase[2] != "")
    {
        thoughtText.text += phrase[2];
        yield return new WaitForSeconds(delay[2]);
    }
    if (phrase[3] != "")
    {
        thoughtText.text += phrase[3];
        yield return new WaitForSeconds(delay[3]);
    }
}
}

Thanks once again anyway for the detailed reply.