How To Use Variable Scopes to Write Cleaner, Bug-Free Arduino Code
2026-06-10 | By Maker.io Staff
Variables in C++ have a type, a name, and a value. However, their scope is easy to overlook and can be a common source of hard-to-trace bugs and unmaintainable code. Read on to learn the basics of variable scopes, the common pitfalls to avoid, and a few actionable steps for writing more reliable and clean Arduino code.

Variables Always Have a Scope
A variable’s scope is the region within a program where that variable is valid and can be accessed. Unlike the type and name, programmers do not have to explicitly state the variable scope. Instead, the scope is determined by where the variable is declared. Within a scope, each variable must be unique. However, it’s possible to redefine variables in different scopes using the same name, which can quickly result in bugs:
int brightness = 90;
void setup() {
Serial.begin(9600);
}
void loop() {
constrainBrightness(brightness++);
Serial.print("Brightness: ");
Serial.println(brightness);
delay(250);
}
void constrainBrightness(float brightness) {
brightness = constrain(brightness, 0, 100);
}
The programmer who wrote this short example defined a brightness variable at the start of the sketch. Since it’s not located within a function or block, it automatically has global scope, meaning that all other functions and blocks can read and modify its value. However, there is a caveat here, which is an often-overlooked source of errors, especially when reusing code.
Notice how the constrainBrightness function declares its own brightness variable as a parameter. Function parameters and variables declared within functions are only valid inside that function, and they can hide global variables with the same name. This is called variable shadowing, and the variable with the more specific scope hides the outer one with the same name. It doesn’t matter whether the two variables have the same type.
The intended behavior of this program was to gradually increase the brightness until it reached 100. However, since the constrainBrightness function and the loop method update different variables, the brightness increases beyond 100:
The serial monitor shows that the global brightness goes above 100, even though the constrainBrightness function is called.
Which Variable Scopes Exist in C?
Each variable defined outside of a block or function automatically has global scope. It’s visible in the entire file that declares it and other files that reference it using extern, unless the variable is declared with the static keyword. This is the least specific scope, and any more specific variable of the same name will shadow it:
// Visible in the current and other files int numberOfActivations = 0; // Visible only in the current file static boolean timerReset = false;
Local variables declared inside a function or as parameters are valid within the declaring function. They shadow global variables with the same name:
int someVariable = 0;
void doSomething(char someVariable, string text) {
int occurrencesInText = 0; // Only valid in doSomething
Serial.printlnt(someVariable); // This prints the char param!
}
Lastly, the block scope is similar to the function scope but restricted to a single block, such as an if-block or a loop. It is even more specific than the function scope, and variables declared within a block shadow function-scoped and global variables of the same name:
int a = 0;
void calculate(double a, double b) {
a = 20.0; // parameter a shadows global a
if (b <= 0) {
string b = “The result is: ”;
double f = a / 2.0;
Serial.print(b);
Serial.println(f);
}
// f does no longer exist outside of the if-block
}
One thing to remember is that the most local variable shadows all more global variables of the same name. Block variables are used before function variables, which take precedence over global variables:
This image illustrates how the most local variables are used before more global ones.
Beyond Scope: Introducing Variable Modifiers
The previous section explained that the static keyword hides global variables from external access. When used with a global variable, the static keyword takes on a scope-altering role. However, when used on local variables with function scope, the modifier affects the variable’s lifetime rather than its scope.
Without the static keyword, function-scoped variables are reset between function calls. However, with the keyword, the function remembers the value and carries it over into subsequent calls. This does not affect the variable’s visibility. Consider this simple example:
void setup() {
Serial.begin(9600);
}
void loop() {
regularCount();
staticCount();
Serial.println();
delay(250);
}
void regularCount() {
int count = 0;
Serial.print("Regular: ");
Serial.println(count++);
}
void staticCount() {
static int count = 0;
Serial.print("Static: ");
Serial.println(count++);
}
The regularCount function will always print a count of zero, since its count variable resets with each new function call. In contrast, the staticCount uses a static count variable, which remembers the current value across function calls:
This screenshot shows the output of the regularCount and staticCount functions.
Other common variable modifiers include unsigned and const. When used on a numeric variable type, the unsigned keyword ensures that the variable only stores positive values. The const modifier can be used to declare constant variables. Their value cannot be updated once assigned.
Finally, the volatile keyword informs the compiler that a variable’s value might change unexpectedly. It enforces that each variable access results in an actual memory operation. It further deactivates the removal of seemingly unused occurrences and caching. This is vital when used in interrupt service routines (ISR) to ensure that the program doesn’t miss asynchronous state changes.
Conclusions and Practical Tips for Working With Variable Scopes
To summarize, a variable’s scope defines the region of a program where a variable can be accessed and used. Or, if you flip the explanation, you can view the scope as a practical tool that lets you limit how much of the program can unintentionally change a variable. Limiting the scope can help prevent errors and make the entire code easier to follow and maintain.
Thus, the first actionable advice is to keep the scope as local as possible. Ideally, global variables should only be used when the program needs to share the state across functions. A classic example is a volatile flag that gets updated in an ISR and then triggers some behavior in the loop method.
Next, static function variables should only be used if the variable value must survive across calls. Otherwise, variables should not be static. If the value has to survive, prefer using static function-scoped variables over global ones.
Opt for function parameters and return values over global variables, which can help reduce major headaches in many cases. However, this doesn’t work as universal advice.
Variable shadowing refers to a problem where a more local variable hides a more global one with the same name. Data access may return unexpected values if hiding was unintentional, which can result in bugs. In either case, you should strive to avoid shadowing, or at the very least, not ignore unintentional shadowing. Variable shadowing is generally regarded as a code smell, and it often hints at larger structural problems in the code.
Global variables that hold a class’s local state should be hidden from other files unless sharing is explicitly wanted to prevent external files from breaking control flows. Similarly, you should not treat global variables as the default. Instead, you should employ them sparingly, and each use should be deliberate.
Lastly, extern is a way to “copy” global variables from other files into the current file so that they effectively become the same variable. You should refrain from doing that unless you know exactly what you’re doing. This pattern is typically not required for most DIY projects. However, it can have valid uses, for example, when a file serves as the single source of truth across an entire library or when libraries communicate internal state (errors, flags, etc.) via such exposed variables.

