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.
If you want to go deeper on the jump from hobby projects to real embedded products, that's what we write about over at Make-it.ai. Tools for hardware design, project breakdowns, and lessons from makers actually shipping things in the wild.
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.