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

ComponentModel / VariantNotes
MicrocontrollerArduino Uno R3 (ATmega328P)5V logic, 16MHz clock
SensorHC-SR04 Ultrasonic5V operating voltage, 2cm-400cm range
Wiring22 AWG solid core jumpers4 wires required
PrototypingHalf-size breadboardStandard 400-point

Pin Mapping Table

HC-SR04 PinArduino Uno R3 PinFunction
VCC5VPower supply
TrigDigital 9Trigger pulse output from Arduino
EchoDigital 10Echo pulse input to Arduino
GNDGNDCommon 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

  1. Missing Return in an Else/Fallback Branch (Most Common): You placed a return inside an if statement, but forgot to provide a default return at the very end of the function. Fix: Always place a default return statement as the absolute last line of the function.
  2. Switch Statement Without Default: Your function returns a value based on a switch case, but lacks a default: case with a return. Fix: Add a default case that returns a safe fallback value or error code.
  3. Implicit Boolean Logic Gaps: You have if (x > 10) return 1; and if (x < 10) return 0; but forgot the condition where x == 10. Fix: Use if / else if / else structures to guarantee full coverage.
The First 3 Things to Check When a Return Fails:
  1. Branch Coverage: Trace every if/else and switch path. Does every single path hit a return keyword?
  2. Type Truncation: Are you returning a float (e.g., 3.14) but the function is declared as int? The compiler will silently truncate it to 3. Change the function signature to match the data.
  3. 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 a struct or 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 float returns and struct overhead. Return raw uint16_t integer 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.