Most Arduino tutorials teach you to blink an LED and stop there. The jump from "it works on my desk" to "it ran for 6 months without crashing" is where most hobby projects die. Here are eight habits that get you across that gap - most of them I learned the hard way.
1. Stop using delay()
delay(1000) halts your entire program for a full second. Nothing else runs - not your buttons, not your sensor reads, not your serial output. Use millis() instead:
unsigned long lastBlink = 0;
const unsigned long BLINK_INTERVAL = 1000;
void loop() {
if (millis() - lastBlink >= BLINK_INTERVAL) {
lastBlink = millis();
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
// your other code keeps running here
}
Once you internalize this pattern, every project gets better.
2. Avoid the String class on AVR boards
String fragments the tiny heap on Uno/Nano/Mega. Your sketch runs fine for an hour, then crashes mysteriously at 3 AM. Use fixed-size char buffers:
char buf[32];
snprintf(buf, sizeof(buf), "Temp: %d C", temperature);
Serial.println(buf);
On ESP32 with megabytes of RAM, String is fine. On a 2KB AVR, it's a timebomb.
3. Wrap string literals in F()
Every Serial.println("Hello world") copies that string into precious RAM at boot. The F() macro keeps it in flash:
Serial.println(F("System ready")); // costs 0 bytes of RAM
On a 2KB Uno, this single change can free hundreds of bytes.
4. Be explicit about integer sizes
int is 16 bits on AVR and 32 bits on ESP32/SAMD. Code that works on an Uno silently breaks on an ESP32 (or vice versa) because of overflow. Use <stdint.h> types:
uint8_t pin = 7;
uint16_t adcValue;
uint32_t timestamp;
int16_t signedTemp;
Your future self porting between boards will thank you.
You almost never need a physical pull-up resistor for a button. The AVR/ESP32 has them built in:
pinMode(BUTTON_PIN, INPUT_PULLUP);
// button pressed = LOW, released = HIGH
Less wiring, fewer parts, fewer things to debug.
6. Keep ISRs tiny and mark shared variables volatile
Interrupt service routines should set a flag and exit. Do the real work in loop():
volatile bool buttonPressed = false;
void IRAM_ATTR onButton() { // IRAM_ATTR for ESP32; omit on AVR
buttonPressed = true;
}
void loop() {
if (buttonPressed) {
buttonPressed = false;
handleButton();
}
}
Calling Serial.print() inside an ISR is a common rookie mistake - it can deadlock the whole board.
7. Model state machines explicitly
If your project has modes - idle, recording, transmitting, error - don't nest five if statements. Use an enum:
enum class State { Idle, Recording, Transmitting, Error };
State state = State::Idle;
void loop() {
switch (state) {
case State::Idle: handleIdle(); break;
case State::Recording: handleRecording(); break;
case State::Transmitting: handleTransmitting(); break;
case State::Error: handleError(); break;
}
}
This one habit makes debugging an order of magnitude easier.
8. Enable the watchdog for anything deployed
A device sitting in your garage with no debugger needs to recover from its own bugs. On AVR:
#include <avr/wdt.h>
void setup() { wdt_enable(WDTO_8S); }
void loop() { wdt_reset(); /* your code */ }
If loop() hangs for more than 8 seconds, the chip reboots itself. Beats driving across town to power-cycle.
A bonus: setup() and loop() are just main()
The Arduino IDE hides it, but underneath it's plain C++. You can - and should - define classes, split code across .h/.cpp files, and use namespaces. The .ino file is just convention.
What's the Arduino habit you wish you'd picked up earlier? Drop it in the comments - I'm curious which of these resonate and which ones people disagree with.