Hello! Welcome back to our course on C pointers.
In our last lesson, we built a functional dynamic array, a significant step in applying your knowledge of dynamic memory. We briefly touched upon the "safe realloc pattern" to avoid memory leaks if resizing fails. Today, we will expand on that single point of safety and dedicate our entire session to a topic of paramount importance in your field of automotive and embedded systems: writing robust code that handles memory allocation failures systematically.
Your experience with functional safety standards like ISO 26262 and ASPICE has already instilled the discipline of anticipating and mitigating failures. This lesson will equip you with the specific C idioms and patterns to apply that discipline to dynamic memory. Our goal is to answer the question: "What should my program do when it runs out of memory?" The answer is more nuanced than simply terminating the program.
By the end of this lesson, you will be able to handle memory allocation failures and write robust error-checking code, turning your functions from prototypes into production-ready, reliable components.
The Fundamental Signal of Failure
As you know, the three main dynamic allocation functions—malloc(), calloc(), and realloc()—all share a common way of signaling failure: they return a NULL pointer.
int *arr = malloc(1000 * sizeof(int));
if (arr == NULL) {
// Allocation failed. We must handle this!
}
This check is the non-negotiable first step in writing robust memory management code. A failure to allocate memory can happen for various reasons, such as requesting an impossibly large block or, more commonly in embedded systems, exhausting the available heap space.
For more detailed error information, these functions often set the global errno variable to ENOMEM ("Not enough memory"). You can use functions like perror() or strerror() to print a human-readable message.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
// ...
int *arr = malloc(SIZE_MAX); // Intentionally try to fail
if (arr == NULL) {
// Option 1: perror prints our message, plus the system error
perror("Failed to allocate memory for array");
// Option 2: strerror gives us the string to format ourselves
fprintf(stderr, "Error: %s\n", strerror(errno));
// We must now decide what to do...
}
The crucial question is, what comes next? Let's explore the primary strategies.
Strategy 1: Terminate the Program
The simplest strategy is to decide that the error is unrecoverable and terminate the program. This is often an acceptable approach for:
- Critical allocations during program startup. If you can't allocate a core data structure, the program simply cannot run.
- Non-critical applications where graceful degradation is not required.
The different ways to handle errors in C
The article 'The different ways to handle errors in C' discusses several error-handling philosophies. We'll start with the most direct one: crashing.
Please read section '2. Crash'. It makes the important point that for an error like a malloc failure, giving up is often the most practical and safest option.
Here is the pattern in practice:
#include <stdio.h>
#include <stdlib.h>
void* safe_malloc(size_t size) {
void* ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Fatal: memory allocation failed. Exiting.\n");
exit(EXIT_FAILURE); // A standard way to indicate unsuccessful termination
}
return ptr;
}
int main() {
// Now we can use safe_malloc without checking the return value in main,
// because the function guarantees it never returns NULL.
int* my_array = safe_malloc(100 * sizeof(int));
// ... proceed with using my_array ...
free(my_array);
return 0;
}
While simple, this "all or nothing" approach isn't suitable for libraries, long-running systems, or any context where the program must attempt to recover or at least clean up before exiting.
Strategy 2: Propagate the Error
A more robust strategy is for the function that failed to allocate memory to report the failure to its caller. This allows the calling function to decide how to handle the error—perhaps it can try a different strategy, free up other memory, or log the error and continue with limited functionality.
The article we just looked at, LINK, discusses two common patterns for this in sections 4. Return NULL and 5. Return a boolean and take an out param. For a function that is supposed to return a dynamically allocated object, returning NULL is the most natural way to signal failure.
The following video demonstrates this when building a function that returns a generated array. It shows why you must use dynamic allocation to return an array created inside a function, which makes handling allocation failures within that function essential.
Dynamic Memory with Malloc - Everything you Need to Know
In the video 'Dynamic Memory with Malloc', Caleb Curry explains why dynamic allocation is necessary when returning an array from a function and how to check for allocation failure in that context.
Please watch the segment from 10:32 to 13:22. Notice how the generate_array function first creates an array on the stack, which is incorrect and produces a warning. Then, it's corrected to use malloc. The key takeaway is the if (array == NULL) check inside the function that is responsible for creating and returning the data.
The Resource Cleanup Challenge
So far, we've handled a single allocation. What happens when a function successfully allocates resource A and resource B, but then fails to allocate resource C?
int process_data() {
char *buffer1 = malloc(100);
if (buffer1 == NULL) {
return -1; // No cleanup needed
}
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
free(buffer1); // Must free buffer1!
return -1;
}
char *buffer2 = malloc(200);
if (buffer2 == NULL) {
fclose(file); // Must close file!
free(buffer1); // Must free buffer1!
return -1;
}
// ... do work ...
// Success case: clean everything up
free(buffer2);
fclose(file);
free(buffer1);
return 0;
}
This code works, but it's already becoming messy. The cleanup logic is duplicated and grows with each new resource. This pattern is error-prone and hard to maintain—a clear violation of the principles of clean, safe code.
The goto Solution: Centralized Cleanup
While goto is often criticized, its use for centralized error handling and cleanup is a widely accepted, idiomatic pattern in professional C programming. It is explicitly recommended by style guides for the Linux Kernel and safety-critical standards you are familiar with, such as MISRA C.
The pattern works by having a single cleanup section at the end of the function. If an error occurs at any step, the code uses goto to jump to the appropriate label in the cleanup chain.
Best practices for non-local exit with cleanup in C?
This Stack Overflow answer discusses best practices for this exact problem. It's an excellent resource that justifies the use of goto by referencing authoritative sources.
Please read the top-voted answer (with '3' votes). Focus on the section that begins 'Error handling is the only place where using goto is actually encouraged' and study the code example that follows. This will show you the canonical goto-based cleanup pattern.
This pattern is so effective that it's used in high-quality, widely-deployed C codebases like SQLite.

The following video provides a fantastic visualization of why this pattern is so much cleaner than nested if statements.
why is it illegal to use "goto"?
The video 'why is it illegal to use "goto"?' from the Low Level channel masterfully contrasts a messy, duplicative error-handling approach with the clean, maintainable goto pattern.
Please watch from 01:51 to 05:16. The video first shows a function that acquires a file, memory, and a socket, demonstrating how the cleanup code becomes duplicated and complex. It then refactors the same function using goto to create a single, clean exit path. This is the core concept of this lesson.
Let's refactor our previous example using this superior pattern:
int process_data_robust() {
int status = -1; // Default to error
char *buffer1 = NULL;
FILE *file = NULL;
char *buffer2 = NULL;
buffer1 = malloc(100);
if (buffer1 == NULL) {
perror("malloc buffer1");
goto cleanup;
}
file = fopen("data.txt", "r");
if (file == NULL) {
perror("fopen");
goto cleanup;
}
buffer2 = malloc(200);
if (buffer2 == NULL) {
perror("malloc buffer2");
goto cleanup;
}
// ... do work ...
status = 0; // Success!
cleanup:
// This section is always executed, on success or failure.
// The functions are safe to call with NULL/closed handles.
if (buffer2) free(buffer2);
if (file) fclose(file);
if (buffer1) free(buffer1);
return status;
}
This version is cleaner, has no code duplication for cleanup, and is much easier to read and verify.
Test your understanding!
Consider the following function, which creates a DynamicArray from the previous lesson. It allocates the main struct and its internal data buffer. Refactor this function to use the goto cleanup pattern to handle allocation failures robustly. The function should return NULL on failure.
// Original (unsafe) function
DynamicArray* DynamicArrayCreate(size_t itemSize, size_t initialCapacity) {
DynamicArray* da = malloc(sizeof(DynamicArray));
da->itemSize = itemSize;
da->size = 0;
da->capacity = initialCapacity;
da->data = malloc(initialCapacity * itemSize);
return da;
}
Show answer
Here is the refactored function using the goto cleanup pattern.
DynamicArray* DynamicArrayCreate_Robust(size_t itemSize, size_t initialCapacity) {
DynamicArray* da = NULL;
da = malloc(sizeof(DynamicArray));
if (da == NULL) {
goto cleanup; // Nothing to clean yet
}
// Initialize fields immediately after allocation
da->data = NULL; // Important: initialize to NULL before next allocation
da->itemSize = itemSize;
da->size = 0;
da->capacity = initialCapacity;
// Handle the case of zero initial capacity
if (initialCapacity > 0) {
da->data = malloc(initialCapacity * itemSize);
if (da->data == NULL) {
goto cleanup; // `da` itself needs to be freed
}
}
return da; // Success
cleanup:
// This section handles cleanup for any failure point.
// If the first malloc failed, `da` is already NULL.
// If the second malloc failed, we free `da`.
if (da) {
free(da);
}
return NULL; // Signal failure to the caller
}
This version ensures that if the second malloc for da->data fails, the memory allocated for the DynamicArray struct itself is correctly freed before returning NULL.
Conclusion
Today you've moved beyond simply checking for NULL and learned how to structure your C functions to be genuinely robust in the face of memory allocation failures. For the systems you build, where reliability is not optional, these patterns are not just "good practice"—they are essential.
Key Takeaways:
- Always check the return value of
malloc(),calloc(), andrealloc()againstNULL. - When an allocation fails, you must choose a clear strategy: either terminate if the error is fatal, or propagate the error to the caller.
- For functions that acquire multiple resources, naive error handling leads to resource leaks and unmaintainable "pyramids of doom".
- The
gotocleanup chain is a standard, professional, and MISRA-compliant C idiom for centralizing resource deallocation, making code safer and more readable.
Preview of the Next Lesson:
In our robust DynamicArrayCreate_Robust function, we returned NULL to signal failure. But what if a function needs to allocate memory for a pointer that was passed in by the caller? In that scenario, the function needs to modify the caller's original pointer. To do this, you can't just pass the pointer itself; you must pass a pointer to the pointer. In our next lesson, we will explore this powerful concept: declaring and using pointers to pointers (double pointers), which unlocks a new level of dynamic memory manipulation and function design.