[Solved] Can someone peer review my Configuration Manager? (ESP8266)

I am writing a WiFi connection setup application on an ESP8266. I am having a problem where every time I save the configuration, usually after some time, it will crash with an output similar to the following: (In this scenario, I chose to skip WiFi setup.)

Configuration was skipped.
Stopping Web Server.
Saving Configuration File.
Mounting File System.
Configuration File Contents: {"skipped":true}
Unmounting File System.

Exception (28):
epc1=0x402106c9 epc2=0x00000000 epc3=0x00000000 excvaddr=0x000002a6 depc=0x00000000

ctx: cont 
sp: 3fff0120 end: 3fff0350 offset: 01a0

>>>stack>>>
3fff02c0:  00000000 3fff2cfc 3fff1c0c 3ffef31c  
3fff02d0:  00000001 3fff1c34 3fff1c0c 4020c955  
3fff02e0:  3fffdad0 00000000 3fff192c 4020e121  
3fff02f0:  00000000 3fff2cfc 40206ee8 40210444  
3fff0300:  3fffdad0 00000000 3ffeefc8 402089d8  
3fff0310:  00000000 00000000 3fff0fc4 402070a7  
3fff0320:  00000000 00000000 3ffef315 40206e63  
3fff0330:  3fffdad0 00000000 3ffef315 4020f458  
3fff0340:  feefeffe feefeffe 3ffef330 40100710  
<<<stack<<<

 ets Jan  8 2013,rst cause:2, boot mode:(3,6)

load 0x4010f000, len 1384, room 16 
tail 8
chksum 0x2d
csum 0x2d
v614f7c32
~ld

Mounting File System.
Loading Configuration File.
Configuration File Contents: {"skipped":true}
Unmounting File System.
Configuration was skipped previously.

The interesting part is that the config file contents are still saved despite the crash. I know this is happening in the save function because when I comment it out, the error goes away.

Would someone be willing to glance over my code and see if there’s a glaring issue?
Here is my source code:

config-manager.h

#ifndef CONFIG_MANAGER_H
#define CONFIG_MANAGER_H

#include <ArduinoJson.h>

namespace Redacted
{

/**
 * Represents the configuration file.
 */
struct Config {
    bool skipped;
};

/**
 * Manages a configuration file, including saving and loading.
 */
class ConfigManager
{

  private:
    ConfigManager();
    ~ConfigManager();

    /**
     * The configuration file.
     */
    Config config;

    /**
     * Loads a JSON object into the config struct.
     *
     * @param json The object to load.
     */
    void loadConfig(JsonObject &json);

    /**
     * Saves the config to memory.
     */
    void saveConfig();

  public:
    /**
     * Gets the singleton instance of this class.
     */
    static ConfigManager &getInstance();

    /**
     * Creates and saves a new, empty configuration file.
     */
    void initEmptyConfig();

    /**
     * Sets whether or not the configuration was skipped.
     *
     * @param skipped True if config was skipped.
     */
    void setSkipped(bool skipped);

    /**
     * Gets whether or not the configuration was skipped.
     * 
     * @returns True if config was skipped.
     */
    bool getSkipped();
};

}; // namespace

#endif

config-manager.cpp

#include "config-manager.h"
#include "FS.h"

Redacted::ConfigManager::ConfigManager()
{
    // Mount the file system.
    Serial.println(F("Mounting File System."));
    if (!SPIFFS.begin())
    {
        Serial.println(F("Failed to mount File System."));
        return;
    }

    // Load config file.
    Serial.println(F("Loading Configuration File."));
    File configFile = SPIFFS.open("/config.json", "r");
    if (!configFile)
    {
        // Could not load config file.
        Serial.println(F("Failed to read Configuration File."));
        initEmptyConfig();
        return;
    }

    // Make sure the size of the config file is not too large for memory.
    size_t size = configFile.size();
    if (size > 1024)
    {
        // Too large.
        Serial.println(F("Configuration File is too large."));
        initEmptyConfig();
        return;
    }

    // Read the config file to a buffer.
    std::unique_ptr<char[]> buf(new char[size]);
    configFile.readBytes(buf.get(), size);

    // Close the file.
    configFile.close();

    // Parse the config file as JSON.
    DynamicJsonBuffer jsonBuffer;
    JsonObject &json = jsonBuffer.parseObject(buf.get());

    // Check if parsing was successful.
    if (!json.success())
    {
        // Parsing failed.
        Serial.println(F("Configuration File could not be parsed as JSON."));
        initEmptyConfig();
        return;
    }

    // Print the json to the serial monitor for debugging.
    String output;
    json.printTo(output);
    Serial.print(F("Configuration File Contents: "));
    Serial.println(output);

    // Load into config struct.
    loadConfig(json);

    // Unmount the file system
    Serial.println(F("Unmounting File System."));
    SPIFFS.end();
}

Redacted::ConfigManager::~ConfigManager()
{
    // Save on shutdown.
    saveConfig();
}

void Redacted::ConfigManager::loadConfig(JsonObject &json)
{
    // Example: https://arduinojson.org/v5/example/config/
    config.skipped = json["skipped"] | false;
}

void Redacted::ConfigManager::saveConfig()
{
    Serial.println(F("Saving Configuration File."));

    // Mount the file system.
    Serial.println(F("Mounting File System."));
    if (!SPIFFS.begin())
    {
        Serial.println(F("Failed to mount File System."));
        return;
    }

    // Open config file for writing.
    File configFile = SPIFFS.open("/config.json", "w");
    if (!configFile)
    {
        // Could not write to config file.
        Serial.println(F("Failed to write Configuration File."));
        return;
    }

    // Create a new json object for writing.
    DynamicJsonBuffer jsonBuffer;
    JsonObject &json = jsonBuffer.createObject();

    // Write struct into json object.
    json["skipped"] = config.skipped;

    String output;
    json.printTo(output);
    Serial.print(F("Configuration File Contents: "));
    Serial.println(output);

    // Write the config json to the file.
    json.printTo(configFile);

    // Close the file.
    configFile.close();

    // Unmount the file system
    Serial.println(F("Unmounting File System."));
    SPIFFS.end();
}

Redacted::ConfigManager &Redacted::ConfigManager::getInstance()
{
    static ConfigManager instance;
    return instance;
}

void Redacted::ConfigManager::initEmptyConfig()
{
    Serial.println(F("Creating new Configuration File."));

    // Create a new json object.
    DynamicJsonBuffer jsonBuffer;
    JsonObject &json = jsonBuffer.createObject();

    if (json == JsonObject::invalid())
    {
        Serial.println(F("Failed to create configuration file."));
        return;
    }

    // Load into config struct.
    loadConfig(json);

    // Save the new config.
    saveConfig();
}

void Redacted::ConfigManager::setSkipped(bool skipped)
{
    config.skipped = skipped;
    saveConfig();
}

bool Redacted::ConfigManager::getSkipped()
{
    return config.skipped;
}

Thank you for your time, I really appreciate the help. I can answer any questions.

rst cause:2

I think this means that the reset was caused by the reset pin. I assume you did not press it, it can also sometimes be caused by power supply issues. What power supply are you using?

PaulRB:

rst cause:2

I think this means that the reset was caused by the reset pin. I assume you did not press it, it can also sometimes be caused by power supply issues. What power supply are you using?

I am using a NodeMCU v1.0 board which is powered by the same USB cable that programs it. The USB cable is in a powered USB hub, but the hub is serving a lot of other devices so it may be limited in power. You're right, I did not press the reset button. I'll see if I can try a separate powered hub, thanks!

The stuffs you are using in your code are very expensive in term of memory. I suspects you encounter lack of memory issue rather than hardware issue.

arduino_new:
I suspects you encounter lack of memory issue rather than hardware issue.

The OP is using a board based on esp8266, which has >64K ram, I don't think the sketch can be using that much.

PaulRB:
The OP is using a board based on esp8266, which has >64K ram, I don't think the sketch can be using that much.

By the symptoms he described, I think it likely is memory problem. And the code only shows his library implementation, not the full sketch. So he may use dynamic memory there too.

I'm very careful throughout the sketch about memory allocation and where I use dynamic objects. I believe that this problem has been solved by using a better power supply. Thanks for the help!