Comparing std and no_std

There are a number of factors which must be considered when choosing between std (esp-idf-hal) and no_std (eg. esp-hal). As stated previously, each approach has its own unique set of advantages and disadvantages. While we can't decide for you, this section will hopefully allow you to make an educated decision.

At present, there are unfortunately certain technical restrictions which may dictate your choice; we hope to have these issues resolved soon. Currently you must use the std approach if you require any of the following:

  • Use of Wi-Fi or Bluetooth on a chip other than ESP32 or ESP32-C3
  • The ability to target any chip other than the ESP32, ESP32-C3, ESP32-S2, ESP32-S3 or the ESP8266

Application Runtimes

In the case of applications (as opposed to libraries) the standard library provides a runtime which handles setting up stack overflow protection, spawning the main thread before an application's main function is invoked, and handling of command-line arguments.

Applications targeting no_std will be responsible for initializing their own runtimes instead. Runtime initialization is generally handled by an external dependency, in our case the riscv-rt and xtensa-lx-rt libraries. You can refer to their READMEs and documentation for more information.

One advantage of not including the default runtime is that you're able to write applications at a lower level. This is possible because the applications will have been linked against the core crate instead of std, which makes no assumptions about the system it is running on. As such, it's possible to write applications like bootloaders, firmware, or even operating system kernels using the no_std approach.


Another interesting property of no_std applications is that we cannot use Rust's default main function as our entry point. It makes certain assumptions which are not neccessarily valid in an embedded context (for example, it expects that command-line arguments exist).

Because of this, you will often see the #![no_main] attribute used to instruct the Rust compiler not to use the default entry point. Runtime crates will provide an #[entry] attribute which can be used to mark a diverging function as the application's entry point instead. For example a minimal application might look something like this:


use riscv_rt::entry;

fn main() -> ! {
    loop {}

Panic Handlers

In addition to specifying the application's entry point, for no_std we must also define a panic handler. The default panic behaviour relies on std, as it prints to standard output.

You are able to define a panic handler manually using the #[panic_handler] attribute. Note that this function's signature must match the example below.


use core::panic::PanicInfo;

fn panic(info: &PanicInfo) -> ! {
    // Your implementation goes here!

Alternatively, there are a number of external dependencies which define various panic handlers for us. Some possible choices are panic-halt, panic-semihosting, or panic-never.

These can be used simply by installing the relevant dependency, and then importing the crate:


use panic_halt as _;

Probably the most convenient panic handler for no_std on esp-hal is esp-backtrace