Skip to main content

ESP-IDF tutorial series: Errors

·7 mins·
ESP32C3 Errors
Author
Francesco Bez
Developer Relations at Espressif
Table of Contents
This article explains error handling in FreeRTOS-based embedded systems, highlighting common C practices and their limitations. It introduces ESP-IDF’s esp_err_t type and error-checking macros, demonstrating how they help manage errors systematically. It shows practical ways to implement error handling in embedded applications.

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.

  1. 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.

  2. Global errno Variable
    The C standard library defines a global variable errno to indicate error conditions set by certain library or system calls (e.g., fopen, malloc). After a function call, a developer can check errno 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-local errno, 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:

  1. 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;)

  2. 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 the esp_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() and esp_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 like ESP_ERROR_CHECK(). This macro evaluates an expression (typically a function call returning esp_err_t), logs a detailed error message if the result is not ESP_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:

  1. 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.

  2. The unlikely function
    This is used as a compiler optimization hint. It tells the compiler that the condition err_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 where ESP_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.

Related

ESP-IDF Tutorials: Basic HTTP server
·7 mins
ESP32C3 HTTP Connectivity
This article shows how to create a simple HTTP server. It explains the functions you need and the setup required. After reading this, you should be able to create your own HTTP server on Espressif devices.
Debugging with ESP-IDF VS Code extension: Part 2
·8 mins
Debugging ESP-IDF ESP32-C3 ESP32-S3 Vscode
This two-part guide shows how to set up VS Code with the ESP-IDF extension to debug Espressif boards using JTAG. In this second part, we will debug a simple project using gdb through Espressif’s VSCode extension. We will explore the debugging options for navigating the code and inspecting the variables.
Debugging with ESP-IDF VS Code extension: Part 1
·9 mins
Debugging ESP-IDF ESP32-C3 ESP32-S3
This two-part guide shows how to set up VS Code with the ESP-IDF extension to debug Espressif boards using JTAG. This first part covers the debugging process, hardware setup and connections, and starting the openOCD server.