Introduction#
In microcontroller-based systems running FreeRTOS, developers work within environments that are both constrained and capable. These platforms provide low-level hardware control alongside the benefits of a lightweight real-time operating system.
C continues to be the primary language in this domain, valued for its efficiency, portability, and direct access to hardware. It enables precise control over performance and resource usage, which is an essential requirement for real-time applications.
However, C also has notable shortcomings. It lacks built-in support for structured error handling and object-oriented programming (OOP), features that can help manage complexity and improve maintainability in large or safety-critical systems. As embedded software becomes more sophisticated, often involving numerous modules, tasks, and hardware interfaces, developers must adopt robust patterns to handle errors and organize code effectively. These needs are especially critical in FreeRTOS-based development, where tasks may fail independently and resources must be carefully managed.
In this article, we will examine how ESP-IDF handles error management within its codebase and how you can leverage these tools in your code.
Error handling in C#
C provides no built-in exception handling, so error management is a manual, discipline-driven part of embedded development. In systems using FreeRTOS, where multiple tasks may run concurrently and shared resources must be protected, robust error handling becomes even more important to ensure system stability and predictable behavior.
Common Techniques#
Over the years, two common techniques for managing errors have emerged to manage errors.
Return Codes
The most widespread method is returning status codes from functions to indicate success or failure. These codes are often:- Integers (
0
for success, non-zero for errors or warnings) - Enumerations representing different error types
NULL
pointers for memory allocation or resource acquisition failures
Each calling function is responsible for checking the return value and taking appropriate action, such as retrying, logging, or aborting.
- Integers (
Global
errno
Variable
The C standard library defines a global variableerrno
to indicate error conditions set by certain library or system calls (e.g.,fopen
,malloc
). After a function call, a developer can checkerrno
to understand what went wrong. It’s typically used like this:FILE *fp = fopen("config.txt", "r"); if (fp == NULL) { printf("File open failed, errno = %d\n", errno); }
However, in embedded systems FreeRTOS,
errno
comes with important caveats:- It is often shared globally, which makes it unsafe in multi-tasking environments.
- Some implementations (like
newlib
with thread-safety enabled) provide thread-localerrno
, but this increases memory usage. - It is rarely used in embedded systems due to its “implicit” nature.
In FreeRTOS-based applications, the use of return code is typically followed approach.
In embedded system design, development frameworks often also define:
Custom error types
Many embedded projects define their own error handling systems, which typically include a consistent error type definitions across modules (e.g.,typedef int err_t;
)Macros for error checking
To reduce repetitive boilerplate code, macros are often used to check errors and handle cleanup in a consistent way:#define CHECK(expr) do { if (!(expr)) return ERR_FAIL; } while (0)
These can help standardize behavior across tasks and improve code readability.
In conclusion, in RTOS-based embedded systems where robustness and reliability are critical, manual error handling must be systematic and consistent. While errno
exists and can be used cautiously, most embedded applications benefit more from explicit well-defined error enums and structured reporting mechanisms.
ESP-IDF approach#
ESP-IDF defines its error codes as esp_err_t
type and provides a couple of error checking macros.
esp_err_t
– Structured error codes
Espressif’s ESP-IDF framework introduces a standardized error handling approach through the use of theesp_err_t
type. This is a 32-bit integer used to represent both generic and module-specific error codes. The framework defines a wide range of constants, such as:#define ESP_OK 0 // Success #define ESP_FAIL 0x101 // Generic failure #define ESP_ERR_NO_MEM 0x103 // Out of memory #define ESP_ERR_INVALID_ARG 0x102 // Invalid argument
The full list is available on the error codes documentation page.
Developers typically write functions that return
esp_err_t
, and use these codes to control program flow or report diagnostics.ESP_OK
is zero and errors can be easily checked like this:esp_err_t ret = i2c_driver_install(...); if (ret != ESP_OK) { printf("I2C driver install failed: %s", esp_err_to_name(ret)); return ret; }
ESP-IDF also provides utilities like
esp_err_to_name()
andesp_err_to_name_r()
to convert error codes into readable strings for logging, which is particularly helpful for debugging.ESP_ERROR_CHECK
– Error checking macro
To reduce repetitive error checking code in testing and examples, ESP-IDF includes macros likeESP_ERROR_CHECK()
. This macro evaluates an expression (typically a function call returningesp_err_t
), logs a detailed error message if the result is notESP_OK
, and aborts the program. You will find this macro repeatedly used in ESP-IDF examples:ESP_ERROR_CHECK(i2c_driver_install(...));
There is also
ESP_ERROR_CHECK_WITHOUT_ABORT
version of this macro which doesn’t stop execution if an error is returned.
Out of curiosity, let’s look at the macro definition
#define ESP_ERROR_CHECK(x) do { \
esp_err_t err_rc_ = (x); \
if (unlikely(err_rc_ != ESP_OK)) { \
abort(); \
} \
} while(0)
At its core, this macro simply checks whether the given value equals ESP_OK
and halts execution if it does not. However, there are two elements that might seem confusing if you’re not familiar with C macros:
The
do { } while (0)
wrapper
This is a common best practice when writing multi-line macros. It ensures the macro behaves like a single statement in all contexts, helping avoid unexpected behavior during compilation. If you’re curious about the reasoning behind this pattern, this article offers a good explanation.The
unlikely
function
This is used as a compiler optimization hint. It tells the compiler that the conditionerr_rc_ != ESP_OK
is expected to be false most of the time—i.e., errors are rare—so it can optimize for the more common case whereESP_OK
is returned. While it doesn’t change the program’s behavior, it can improve performance by guiding branch prediction and code layout.
Examples#
In this section, we’ll start with a simple, self-contained example to demonstrate how error codes and macros work in practice. Then, we’ll examine how these concepts are applied in an actual ESP-IDF example by reviewing its source code.
Basic Example: Division#
To demonstrate the use of the error codes, we will implement a division function.
Unlike other basic operations, division can result in an error if the second argument is zero, which is not allowed. To handle this case, we use an esp_err_t
return type for error reporting.
The division function is implemented as follows:
esp_err_t division(float * result, float a, float b){
if(b==0 || result == NULL){
return ESP_ERR_INVALID_ARG;
}
*result = a/b;
return ESP_OK;
}
As you can see, since the function’s return type is esp_err_t
, we need an alternative way to return the division result. The standard approach is to pass a pointer to a result variable as an argument. While this may seem cumbersome for a simple application, its advantages become increasingly clear when applying object-oriented programming (OOP) principles in C.
In the app_main
function, we first check for errors before printing the result.
void app_main(void)
{
printf("\n\n*** Testing Errors ***!\n\n");
float division_result = 0;
float a = 10.5;
float b = 3.3;
if(division(&division_result,a,b)==ESP_OK){
printf("Working division: %f\n", division_result);
}else{
printf("Division Error!\n");
}
b = 0;
if(division(&division_result,a,b)==ESP_OK){
printf("Working division: %f\n", division_result);
}else{
printf("Division Error!\n");
}
}
And the result is as expected
*** Testing Errors ***!
Working division: 3.181818
Division Error!
We can also use ESP_ERROR_CHECK_WITHOUT_ABORT(division(&division_result,a,b))
instead of the if/else block.
It results is a silent pass for the first function call and in the following message for the second.
ESP_ERROR_CHECK_WITHOUT_ABORT failed: esp_err_t 0x102 (ESP_ERR_INVALID_ARG) at 0x4200995a
--- 0x4200995a: app_main at <folder_path>/error-example/main/main.c:37
file: "./main/main.c" line 37
func: app_main
expression: division(&division_result, a, b)
Using ESP_ERROR_CHECK
makes the system reboot after the error is found.
ESP-IDF example#
Let’s examine a more complete example taken directly from the ESP-IDF example folder. The following code is from the HTTPS request example.
The app_main
function code is as follows
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
//[...]
ESP_ERROR_CHECK(example_connect());
if (esp_reset_reason() == ESP_RST_POWERON) {
ESP_ERROR_CHECK(update_time_from_nvs());
}
const esp_timer_create_args_t nvs_update_timer_args = {
.callback = (void *)&fetch_and_store_time_in_nvs,
};
esp_timer_handle_t nvs_update_timer;
ESP_ERROR_CHECK(esp_timer_create(&nvs_update_timer_args, &nvs_update_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(nvs_update_timer, TIME_PERIOD));
xTaskCreate(&https_request_task, "https_get_task", 8192, NULL, 5, NULL);
}
As you can see, almost all function calls are surrounded by the ESP_ERROR_CHECK
macro.
ESP_ERROR_CHECK
is used only in examples and prototypes because it aborts execution on error. It should not be used in production code, where a properly designed error-handling mechanism is preferred.Conclusion#
In this article, we examined error handling in FreeRTOS-based embedded systems, focusing on the ESP-IDF framework. We covered common C techniques, the importance of systematic error management, and how ESP-IDF uses esp_err_t
and macros to simplify error checking. Through both a simple example and a real-world ESP-IDF example, we saw practical applications of these concepts to improve code robustness and reliability.