To return a value from a function in Arduino, change the function's return type from void to your desired data type (like int, float, or bool), and use the return keyword followed by the variable or expression. The calling code must then assign this result to a variable or use it directly in a conditional statement.
While the syntax is standard C++, the constraints of microcontroller memory (like the 2KB SRAM on the ATmega328P) and the strictness of the Arduino IDE 2.x GCC compiler introduce specific pitfalls. This guide covers the exact syntax, a practical hardware build, compiler error debugging, and advanced memory-safe return techniques.
Project Build: Ultrasonic Distance Sensor with Return Functions
To demonstrate function returns in a real-world scenario, we will wire an HC-SR04 ultrasonic sensor to an Arduino Uno R3. The custom function will trigger the sensor, measure the echo pulse, calculate the distance, and return a float. Crucially, it includes error handling to return a specific fault code (-1.0) if the sensor times out or is disconnected.
Parts List & Specifications
| Component | Model / Variant | Notes |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | 5V logic, 16MHz clock |
| Sensor | HC-SR04 Ultrasonic | 5V operating voltage, 2cm-400cm range |
| Wiring | 22 AWG solid core jumpers | 4 wires required |
| Prototyping | Half-size breadboard | Standard 400-point |
Pin Mapping Table
| HC-SR04 Pin | Arduino Uno R3 Pin | Function |
|---|---|---|
| VCC | 5V | Power supply |
| Trig | Digital 9 | Trigger pulse output from Arduino |
| Echo | Digital 10 | Echo pulse input to Arduino |
| GND | GND | Common ground reference |
Complete Compilable Code
// Target Board: Arduino Uno R3 (ATmega328P)
// IDE Version: Arduino IDE 2.x
#define TRIG_PIN 9
#define ECHO_PIN 10
#define TIMEOUT_US 25000 // ~4 meters max range timeout
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
// Ensure trigger pin is low on startup
digitalWrite(TRIG_PIN, LOW);
}
void loop() {
float distance = getDistanceCM();
// Error handling based on returned value
if (distance < 0.0) {
Serial.println("ERROR: Sensor timeout or disconnected.");
} else {
Serial.print("Distance: ");
Serial.print(distance);
Serial.println(" cm");
}
delay(250); // 4Hz read rate prevents echo interference
}
// Function that returns a float value
float getDistanceCM() {
// 1. Send 10us trigger pulse
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// 2. Read echo pulse with timeout
unsigned long duration = pulseIn(ECHO_PIN, HIGH, TIMEOUT_US);
// 3. Error handling: pulseIn returns 0 on timeout
if (duration == 0) {
return -1.0; // Return fault code
}
// 4. Calculate and return distance
// Speed of sound is ~343 m/s (0.0343 cm/us). Divide by 2 for round trip.
float distance = (duration * 0.0343) / 2.0;
return distance;
}
Debugging: "Control Reaches End of Non-Void Function"
When you change a function from void to a specific type like int or float, the Arduino IDE 2.x compiler (which uses GCC under the hood) enforces strict return paths. If your code has conditional logic, you will frequently encounter this exact compiler error:
error: control reaches end of non-void function [-Werror=return-type]
Ranked Causes and Fixes
- Missing Return in an Else/Fallback Branch (Most Common): You placed a
returninside anifstatement, but forgot to provide a defaultreturnat the very end of the function. Fix: Always place a default return statement as the absolute last line of the function. - Switch Statement Without Default: Your function returns a value based on a
switchcase, but lacks adefault:case with a return. Fix: Add a default case that returns a safe fallback value or error code. - Implicit Boolean Logic Gaps: You have
if (x > 10) return 1;andif (x < 10) return 0;but forgot the condition wherex == 10. Fix: Useif / else if / elsestructures to guarantee full coverage.
- Branch Coverage: Trace every
if/elseandswitchpath. Does every single path hit areturnkeyword? - Type Truncation: Are you returning a
float(e.g., 3.14) but the function is declared asint? The compiler will silently truncate it to 3. Change the function signature to match the data. - Scope Death (Dangling Pointers): Are you trying to return a local array (e.g.,
char myArray[10]; return myArray;)? The array is destroyed when the function exits. Return astructor pass the array into the function via pointer instead.
Advanced Technique: Returning Multiple Values via Structs
A common question is how to return multiple values (like temperature and humidity from a DHT22) from a single function. In C++, you cannot use the comma operator like return temp, hum;—it will only return the last variable. While you could use global variables, that destroys encapsulation and makes debugging a nightmare.
The professional approach is to define a struct (structure) and return that. According to the C++ standard reference, structs are value types, meaning they are safely copied to the caller's stack frame upon return.
// Define the structure globally
struct SensorData {
float temperature;
float humidity;
bool isValid;
};
// Function returns the struct
SensorData readDHT22(int pin) {
SensorData data;
// ... hypothetical DHT library read logic ...
float t = 24.5;
float h = 45.2;
if (isnan(t) || isnan(h)) {
data.isValid = false;
data.temperature = 0.0;
data.humidity = 0.0;
} else {
data.isValid = true;
data.temperature = t;
data.humidity = h;
}
return data; // Returns the entire struct safely
}
void loop() {
SensorData readings = readDHT22(2);
if (readings.isValid) {
Serial.println(readings.temperature);
}
}
How to Extend or Simplify This Build
- Simplify: If memory is critically tight (e.g., on an ATtiny85), drop the
floatreturns andstructoverhead. Return rawuint16_tinteger values (e.g., distance in millimeters instead of centimeters) and do the math on the receiving end or in the serial monitor. - Extend: Instead of returning a struct, use pass-by-reference. By passing variables with an
&(e.g.,void readSensor(float &temp, float &hum)), the function modifies the original variables directly. This avoids the minor CPU overhead of copying a struct onto the stack, which matters in high-frequency interrupt service routines (ISRs).
FAQ: Arduino Function Return Questions
Can I return an array from a function in Arduino?
No, not directly. In C++, arrays decay into pointers when passed around. If you declare an array locally inside a function (int arr[5];) and try to return it, you are returning a pointer to memory that is immediately destroyed when the function exits, resulting in garbage data. To "return" an array, either pass an empty array into the function as a parameter for it to fill, use the std::array wrapper (if using modern C++ standards in Arduino IDE 2.x), or return a struct that contains the array.
Why is my returned float value losing decimal precision?
This happens due to implicit type casting. If your function is declared as int getReading() but you attempt to return 3.14;, the compiler truncates the decimal and returns 3. Furthermore, ensure you are doing floating-point math inside the function. Dividing two integers (e.g., return 5 / 2;) performs integer division and returns 2. You must force float math by using decimals: return 5.0 / 2.0;.
How do I return a String object without causing memory fragmentation?
Returning the Arduino String class (capital 'S') from a function creates a new object on the heap. Doing this repeatedly inside loop() causes severe heap fragmentation on the ATmega328P, eventually leading to random crashes. As noted in the official Arduino Language Reference, it is vastly superior to pass a character array (char[]) into the function and use snprintf() to format your data, or return standard C-strings (const char*) stored in PROGMEM.
What is the difference between passing by reference and returning a value?
Returning a value passes data back up the call stack, creating a copy of the data in the calling function's scope. Passing by reference (using &) gives the function direct memory access to the original variable, modifying it in place. Returning is cleaner for single values and mathematical operations; passing by reference is mandatory when you need to modify multiple large data structures without duplicating them in limited SRAM.






