The loop() function is the beating heart of every Arduino sketch. Under the hood, the Arduino core wraps your loop() code in an infinite while(1) C++ construct. If your code executes cleanly, the microcontroller cycles through it thousands of times per second. But if you introduce blocking delays, infinite while loops without exit conditions, or I2C bus lockups, your looping Arduino sketch will hang, drop sensor readings, or trigger a hardware reset.

This guide strips away the beginner delay() crutches and teaches you how to architect a professional, non-blocking state machine. We will also cover how to implement a hardware Watchdog Timer (WDT) to automatically recover from fatal loop hangs, and how to debug the exact error strings that appear when things go wrong.

The Anatomy of a Robust Looping Arduino Sketch

A responsive Arduino sketch never stops to wait. If you use delay(1000) to wait for a sensor to settle, your microcontroller is entirely blind to button presses, serial commands, or network packets for that entire second.

The industry-standard solution is time-slicing via millis(). Instead of pausing the CPU, you record a timestamp, let the loop() continue spinning, and only execute your sensor logic when the difference between the current time and your timestamp exceeds your target interval. This approach is critical to understand, especially when dealing with the 49.7-day millis() rollover, which we handle in the code below using unsigned long subtraction.

Target Board Variant: This code and hardware guide specifically targets the Arduino Uno Rev3 (ATmega328P) running at 16MHz. The Watchdog Timer implementation uses the standard <avr/wdt.h> library. If you are using an Arduino Uno R4 (Renesas core) or an ESP32, the WDT API differs significantly; see the FAQ for migration notes.

Hardware Spec Sheet and Pin Mapping

For this build, we are creating an environmental failsafe controller. It reads a DHT22 temperature/humidity sensor and triggers a 5V relay if the temperature exceeds a threshold. We use 22 AWG solid-core hook-up wire for all breadboard connections.

Component Exact Variant / Model Arduino Pin Notes & Wiring
Microcontroller Arduino Uno Rev3 (ATmega328P) N/A Power via USB or 7-12V DC barrel jack.
Sensor DHT22 (AM2302) D2 Requires 4.7kΩ pull-up resistor between VCC and Data.
Relay Module Songle SRD-05VDC-SL-C (Active LOW) D8 Opto-isolated module. JD-VCC jumper removed for true isolation.
Status LED 5mm Red LED + 220Ω Resistor D13 Acts as a software heartbeat indicator.
⚠️ Mains Voltage Safety Warning: The Songle relay is rated for 10A at 120V/240V AC. If you are switching mains-powered loads (like a space heater or AC fan), never expose bare screw terminals. Use a properly grounded, fused enclosure. If you are not comfortable with mains wiring, stick to switching 12V DC loads like PC fans or LED strips.

Compilable Code: Non-Blocking State Machine with Watchdog

The following sketch uses an enum to manage states and millis() for non-blocking timing. It also initializes the ATmega328P hardware Watchdog Timer. If the loop() hangs for more than 2 seconds (e.g., an I2C sensor locks the SDA line low), the WDT will hard-reset the board.

#include <DHT.h>
#include <avr/wdt.h>

// --- Pin Definitions ---
#define DHT_PIN       2
#define RELAY_PIN     8
#define HEARTBEAT_PIN 13

// --- Constants ---
#define DHT_TYPE      DHT22
#define TEMP_THRESHOLD 28.5  // Celsius
#define READ_INTERVAL  2000  // Read sensor every 2 seconds

// --- State Machine ---
enum SystemState {
  STATE_INIT,
  STATE_READ_SENSOR,
  STATE_EVALUATE,
  STATE_ACTUATE,
  STATE_IDLE
};

SystemState currentState = STATE_INIT;

// --- Objects & Variables ---
DHT dht(DHT_PIN, DHT_TYPE);
unsigned long previousMillis = 0;
unsigned long heartbeatMillis = 0;
bool heartbeatState = false;
float currentTemp = 0.0;
bool sensorError = false;

void setup() {
  Serial.begin(115200);
  
  // Check Reset Reason (Crucial for debugging WDT hangs)
  byte mcusr = MCUSR;
  MCUSR = 0; // Clear register
  if (mcusr & (1 << WDRF)) {
    Serial.println("RESET_REASON: Watchdog Timer Triggered!");
  } else if (mcusr & (1 << PORF)) {
    Serial.println("RESET_REASON: Power-On Reset");
  }

  pinMode(RELAY_PIN, OUTPUT);
  pinMode(HEARTBEAT_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH); // Active LOW relay: HIGH = OFF
  
  dht.begin();
  Serial.println("System Initialized. Entering main loop.");
  
  // Enable 2-second Watchdog Timer
  wdt_enable(WDTO_2S);
}

void loop() {
  // 1. Feed the watchdog immediately to prevent premature resets
  wdt_reset();
  
  // 2. Non-blocking Heartbeat (proves loop is spinning)
  if (millis() - heartbeatMillis >= 500) {
    heartbeatMillis = millis();
    heartbeatState = !heartbeatState;
    digitalWrite(HEARTBEAT_PIN, heartbeatState);
  }

  // 3. State Machine Execution
  switch (currentState) {
    case STATE_INIT:
      previousMillis = millis();
      currentState = STATE_IDLE;
      break;

    case STATE_IDLE:
      // Wait for interval without blocking
      if (millis() - previousMillis >= READ_INTERVAL) {
        previousMillis = millis();
        currentState = STATE_READ_SENSOR;
      }
      break;

    case STATE_READ_SENSOR:
      currentTemp = dht.readTemperature();
      // Error Handling: Check for NaN (Not a Number)
      if (isnan(currentTemp)) {
        Serial.println("ERROR: DHT22 Timeout or Checksum Fail (NaN)");
        sensorError = true;
        currentState = STATE_IDLE; // Skip actuation on bad data
      } else {
        sensorError = false;
        currentState = STATE_EVALUATE;
      }
      break;

    case STATE_EVALUATE:
      if (currentTemp > TEMP_THRESHOLD) {
        currentState = STATE_ACTUATE;
      } else {
        // Ensure relay is off if below threshold
        digitalWrite(RELAY_PIN, HIGH); 
        currentState = STATE_IDLE;
      }
      break;

    case STATE_ACTUATE:
      Serial.print("ALERT: Temp ");
      Serial.print(currentTemp);
      Serial.println("C exceeds threshold. Engaging Relay.");
      digitalWrite(RELAY_PIN, LOW); // Active LOW: LOW = ON
      currentState = STATE_IDLE;
      break;
  }
}

Debugging: When Your Looping Arduino Hangs or Resets

When a looping Arduino sketch fails, it rarely does so silently. Here are the exact error strings and symptoms you will encounter, ranked by probability.

1. Symptom: Serial Monitor Prints Garbage Characters on Boot

Exact Error String: ⸮⸮⸮ or RESET_REASON: Watchdog Timer Triggered!

Ranked Causes:

  1. WDT Reset Loop: Your loop() is taking longer than 2 seconds to execute. The WDT resets the board, it boots, hits the same hang, and resets again. Fix: Comment out wdt_enable() temporarily and use Serial.print() statements to find the blocking line.
  2. Baud Rate Mismatch: Code specifies 115200 but Serial Monitor is set to 9600.
  3. Brownout: The relay coil is drawing too much current from the 5V rail, dropping the ATmega328P voltage below 2.7V. Fix: Power the relay module's JD-VCC pin from a separate 5V buck converter.

2. Symptom: Loop Runs, But Relay Never Triggers

Exact Error String: ERROR: DHT22 Timeout or Checksum Fail (NaN)

Ranked Causes:

  1. Missing Pull-up Resistor: The DHT22 requires a 4.7kΩ resistor between VCC and Data. Without it, the data line floats, causing checksum failures.
  2. Interrupt Conflict: The DHT library relies on precise microsecond timing. If you have a high-frequency interrupt (like a rotary encoder) running, it will break the DHT read. Fix: Disable interrupts during the read or switch to an I2C sensor like the BME280.

The First Three Things to Check When the Loop Fails

If your sketch compiles and uploads but the board becomes unresponsive, run through this triage checklist before rewriting your code:

  1. Hunt for Blocking Functions: Use your IDE's search (Ctrl+F) to find delay(, while(!Serial), or while(digitalRead(...) == LOW). Any while loop that relies on external hardware state without a timeout counter is a fatal trap.
  2. Check for Dynamic Memory Leaks: Are you using the String class (capital 'S') inside the loop()? Every time you concatenate a String inside an infinite loop, it fragments the SRAM heap. Within minutes, the ATmega328P will run out of its 2KB RAM and crash. Fix: Use C-style char arrays or Serial.print() chaining.
  3. Verify I2C Bus Pull-ups: If your loop hangs specifically when calling Wire.requestFrom(), your I2C bus is locked. The SDA line is being held low by a slave device. Ensure you have 4.7kΩ pull-up resistors on both SDA and SCL.

Extending and Simplifying the Build

To Extend: You can easily add a STATE_SLEEP to the enum. By integrating the Low-Power library, you can put the ATmega328P into Power-Down sleep mode between sensor reads, dropping current consumption from 45mA to under 0.1mA. Remember to configure an external interrupt on Pin 2 to wake the WDT.

To Simplify: If you are just building a basic blinking LED or a simple motor test and do not need production-level reliability, delete the #include <avr/wdt.h> line and all wdt_ function calls. The watchdog is a safety net for deployed hardware; it is often a nuisance during active bench prototyping.

FAQ: Looping Arduino Long-Tail Questions

Why is my looping Arduino sketch slowing down over time?

If your loop cycles take progressively longer to complete, you are likely suffering from SRAM heap fragmentation caused by the String object, or you are failing to close I2C/SPI transactions. Another common culprit is the millis() rollover. If you write if (millis() > previousMillis + interval) instead of if (millis() - previousMillis >= interval), your code will break exactly 49.7 days after boot when the 32-bit unsigned long rolls over to zero.

How do I exit the Arduino loop() function completely?

You cannot "exit" the loop() function in the traditional software sense; the Arduino core mandates an infinite execution cycle. However, you can effectively halt the loop by entering an infinite while(1){} block at the end of your code, or by putting the microcontroller into a deep sleep state without a wake interrupt. For safety-critical applications, halting the loop without triggering a WDT reset is considered bad practice.

Can I run two infinite loops on a standard Arduino Uno?

No. The ATmega328P is a single-core, single-threaded microcontroller. It can only execute one instruction at a time. You cannot run two separate while(1) loops concurrently. To simulate multitasking, you must use the non-blocking state machine pattern demonstrated in this article, or upgrade to a dual-core board like the ESP32, which supports FreeRTOS tasks.

What causes the "WDT reset" error in a looping Arduino on ESP32 or Uno R4?

On the classic Uno (ATmega328P), the WDT is opt-in via <avr/wdt.h>. However, on the ESP32 and Arduino Uno R4, a Task Watchdog Timer (TWDT) is often enabled by default in the core RTOS. If your loop() contains a blocking delay longer than 5 seconds (ESP32) or fails to yield CPU time, the OS will trigger a panic. On the ESP32, this prints: Guru Meditation Error: Core 1 panic'ed (Task watchdog got triggered). The fix is to insert yield(); or delay(1); inside heavy computation loops to feed the RTOS watchdog.