# Creating FreeRTOS Tasks in Zig with ESP-IDF: A Complete Guide

> Learn to create FreeRTOS tasks in Zig with ESP-IDF using type-safe wrappers. Explore sample code for idiomatic error handling and efficient task management.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: tutorial
- Published: 2026-03-05

---

**The `kassane/zig-esp-idf-sample` repository provides a type-safe Zig wrapper around FreeRTOS that exposes `Task.create` and `Task.createPinnedToCore` methods, returning error unions for idiomatic Zig error handling while mapping directly to `xTaskCreate` and `xTaskCreatePinnedToCore` underneath.**

Creating FreeRTOS tasks using Zig with ESP-IDF combines the memory safety and error handling of Zig with the real-time capabilities of FreeRTOS on ESP32 microcontrollers. The `kassane/zig-esp-idf-sample` project demonstrates how to wrap the C-based FreeRTOS API in idiomatic Zig abstractions, allowing developers to spawn concurrent tasks without sacrificing type safety or Zig's robust error propagation mechanisms.

## Understanding the FreeRTOS Task Wrapper Architecture

The wrapper implementation resides in `imports/rtos.zig` (lines 45-63), where the `Task` namespace encapsulates all FreeRTOS task management functionality. This design abstracts the raw C pointers and macros into Zig types while maintaining zero-cost interoperability with the underlying ESP-IDF implementation.

### Core Types and Handles

The wrapper defines explicit type aliases that bridge Zig and FreeRTOS semantics:

- **`Task.Handle`**: Maps to `TaskHandle_t`, representing an opaque pointer to the task control block
- **`Task.Function`**: Matches `sys.TaskFunction_t`, requiring the signature `fn (?*anyopaque) callconv(.C) void`
- **`TaskCreateFailed`**: A Zig error set member returned when `xTaskCreate` returns `errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY`

### Mapping to the Underlying C API

The architecture maintains a direct 1:1 relationship between Zig wrapper methods and FreeRTOS C functions:

| Zig Method | C API | Return Type |
|------------|-------|-------------|
| `Task.create` | `xTaskCreate` | `!Handle` (error union) |
| `Task.createPinnedToCore` | `xTaskCreatePinnedToCore` | `!Handle` |
| `Task.delete` | `vTaskDelete` | `void` |
| `Task.delay` | `vTaskDelay` | `void` |
| `Task.notify` | `xTaskGenericNotify` | `!void` |

## Creating FreeRTOS Tasks Using Zig

The `kassane/zig-esp-idf-sample` repository demonstrates two primary patterns for task creation: dynamic allocation with `Task.create` and core-affined allocation with `Task.createPinnedToCore`.

### Basic Task Creation with Task.create

The standard method for spawning tasks follows Zig's error-handling idioms while accepting parameters similar to the C API:

```zig
_ = idf.rtos.Task.create(
    ledStripTask,               // Task function pointer
    "led_strip",               // Task name (for debugging)
    1024 * 4,                  // Stack depth in bytes
    led_strip,                 // Parameter passed to task (optional)
    5,                         // Priority (higher number = higher priority)
) catch @panic("Error: LED strip task not created!");

```

This example from `main/examples/smartled-rgb.zig` (line 84) demonstrates the typical error-handling pattern: using `catch` to convert the Zig error into a panic, though production code might alternatively log the error and return early.

### Pinning Tasks to Specific Cores

For dual-core ESP32 variants (ESP32, ESP32-S3, ESP32-P4), the wrapper exposes `xTaskCreatePinnedToCore` through `Task.createPinnedToCore`:

```zig
const handle = try idf.rtos.Task.createPinnedToCore(
    myTask,      // Task function
    "my_task",   // Name
    2048,        // Stack size
    null,        // No parameter
    3,           // Priority
    0            // Core ID (0 or 1)
);

```

The sixth parameter specifies the target core (0 for PRO_CPU, 1 for APP_CPU on classic ESP32). Passing `tskNO_AFFINITY` (or its Zig equivalent) allows FreeRTOS to schedule the task on either core.

### Error Handling Patterns

Unlike the C API which returns `pdPASS` or `errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY`, the Zig wrapper converts these into Zig's error union system:

```zig
// From main/examples/ble-gatt-server.zig (lines 20-22)
_ = idf.rtos.Task.create(
    &counterTask, "ble_counter", 1024 * 3, null, 5
) catch {
    log.err("Failed to create counter task", .{});
    return;
};

```

This pattern allows developers to use `try`, `catch`, or `if` expressions to handle allocation failures gracefully, integrating naturally with Zig's broader error-handling ecosystem.

## Managing Task Lifecycle and Utilities

Beyond creation, the `Task` namespace provides Zig-idiomatic wrappers for deletion, delays, and inter-task communication.

### Deleting Tasks and Memory Management

The `Task.delete` method wraps `vTaskDelete`, accepting an optional handle:

```zig
// Delete a specific task
idf.rtos.Task.delete(task_handle);

// Delete the calling task (pass null)
idf.rtos.Task.delete(null);

```

When passed `null`, FreeRTOS deletes the calling task and automatically cleans up stack memory. The wrapper maintains this behavior while providing Zig's type safety—ensuring the handle is either a valid `Task.Handle` or `null`.

### Delay and Notification Helpers

The wrapper provides convenience methods for task timing:

```zig
// Delay for a number of FreeRTOS ticks
idf.rtos.Task.delay(100);

// Delay for milliseconds (converts using msToTicks)
idf.rtos.Task.delayMs(500);

```

For inter-task communication, `Task.notify` and `Task.notifyWait` wrap the `xTaskGenericNotify` family, converting the C return codes into Zig error unions for consistent error handling across the API.

## Real-World Examples from the Repository

The `kassane/zig-esp-idf-sample` project demonstrates practical task creation patterns in its example applications.

### LED Strip Controller Task

The smart LED example in `main/examples/smartled-rgb.zig` (line 84) spawns a dedicated task to handle LED strip updates asynchronously:

```zig
_ = idf.rtos.Task.create(
    ledStripTask,
    "led_strip",
    1024 * 4,
    led_strip,
    5,
) catch @panic("Error: LED strip task not created!");

```

This pattern isolates timing-sensitive LED operations from the main application logic, allowing the main thread to continue initialization while the LED task runs concurrently.

### BLE GATT Server Counter Task

The Bluetooth Low Energy example in `main/examples/ble-gatt-server.zig` (lines 20-22) demonstrates error handling when creating a background notification task:

```zig
_ = idf.rtos.Task.create(
    &counterTask, "ble_counter", 1024 * 3, null, 5
) catch {
    log.err("Failed to create counter task", .{});
    return;
};

```

Here, the task increments a counter and sends GATT notifications independently of the main BLE stack processing, showcasing how to handle task creation failures gracefully without panicking.

## Summary

Creating FreeRTOS tasks using Zig with ESP-IDF combines the real-time capabilities of FreeRTOS with Zig's modern type system and error handling. Key takeaways from the `kassane/zig-esp-idf-sample` implementation include:

- The `idf.rtos.Task` namespace in `imports/rtos.zig` provides thin, zero-cost wrappers around `xTaskCreate` and `xTaskCreatePinnedToCore`
- Task creation returns Zig error unions (`!Handle`) rather than C-style error codes, enabling idiomatic `try` and `catch` patterns
- Use `Task.createPinnedToCore` to bind tasks to specific CPU cores on dual-core ESP32 variants
- The optional parameter in `Task.delete` allows tasks to self-terminate by passing `null`
- Real-world examples in `smartled-rgb.zig` and `ble-gatt-server.zig` demonstrate proper stack sizing and error handling patterns

## Frequently Asked Questions

### How does the Zig wrapper handle FreeRTOS task priorities?

The Zig wrapper passes the priority parameter directly to the underlying `xTaskCreate` C function without modification or validation. As shown in `main/examples/smartled-rgb.zig`, you specify priority as a numeric value (typically 1-25 on ESP32), where higher numbers indicate higher priority. The wrapper maintains the same scheduling semantics as the native ESP-IDF FreeRTOS implementation.

### Can I pass custom data structures to a FreeRTOS task in Zig?

Yes, the fourth parameter of `Task.create` accepts an optional opaque pointer (`?*anyopaque`). You can cast a pointer to your Zig struct to `*anyopaque` when creating the task, then cast it back inside the task function. The smart LED example in `main/examples/smartled-rgb.zig` demonstrates this pattern by passing a LED strip configuration pointer to the `ledStripTask` function.

### What is the difference between Task.create and Task.createPinnedToCore?

`Task.create` wraps `xTaskCreate` and allows the FreeRTOS scheduler to run the task on any available CPU core, while `Task.createPinnedToCore` wraps `xTaskCreatePinnedToCore` and binds the task to a specific core (0 or 1 on dual-core ESP32 variants). Use `createPinnedToCore` when you need to ensure a task runs on a specific core for cache locality or to avoid migration overhead, as shown in the wrapper definition in `imports/rtos.zig`.

### How do I handle task creation failures in Zig compared to C?

In C, `xTaskCreate` returns `pdPASS` (1) on success or `errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY` on failure. The Zig wrapper converts these into a proper error union (`!Handle`), returning the task handle on success or `error.TaskCreateFailed` on failure. This allows you to use Zig's standard error handling mechanisms such as `try`, `catch`, or `if` expressions, as demonstrated in `main/examples/ble-gatt-server.zig` where the code logs an error message and returns early rather than crashing the system.