Blog banner

Coding Like NASA: 10 Rules for Writing Safer Software

“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”

Coding Like NASA: 10 Rules for Writing Safer Software

“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”
Linus Torvalds
Photo by Austin Kehmeier on Unsplash

Table of Content

  • Introduction
  • 10 rules to code like NASA ?
  • Rule 1 — Avoid complex flow constructs, such as goto and recursion.
  • Rule 2— All loops must have fixed bounds. This prevents runaway code.
  • Rule 3— Avoid heap memory allocation after initialization.
  • Rule 4— Restrict functions to a single printed page.
  • Rule 5— Use a minimum of two runtime assertions per function.
  • Rule 6— Restrict the scope of data to the smallest possible.
  • Rule 7— Check the return value of all non-void functions, or cast to void to indicate the return value is useless.
  • Rule 8— Use the preprocessor only for header files and simple macros.
  • Rule 9— Limit pointer use to a single dereference, and do not use function pointers.
  • Rule 10 — Compile with all possible warnings active; all warnings should then be addressed before release of the software.
  • Making a conclusion

Introduction

The Power of 10 Rules were created in 2006 by Gerard J. Holzmann of the NASA/JPL Laboratory for Reliable Software.[1] The rules are intended to eliminate certain C coding practices that make code difficult to review or statically analyze. These rules are a complement to the MISRA C guidelines and have been incorporated into the greater set of JPL coding standards.[2]


10 rules to code like NASA ?

If you want to increase the security of your embedded systems, you could start coding like NASA, whose mission-critical applications, distributed in the unforgiving environment of space, require severe security measures. Strict coding practices with tight rules and requirements prevent errors from the outset and keep embedded software secure.

Since 2006, NASA has followed 10 rules they developed, which are testable, readable, and reliable, from an article by Gerard Holzmann of NASA JPL’s Laboratory for Reliable Software titled, “The Power of 10: Rules for Developing Safety-Critical Code.

Coding standards help programming teams to code more cleanly and adhere to coding best practices so that quality is ensured at all times through the software development lifecycle. Like most guidelines for safety-critical, the NASA power of 10 coding rules are primarily targeted at the C programming language and help the developers ensure the reliability of mission-critical software applications developed in C.


Rule 1 — Avoid complex flow constructs, such as goto and recursion.

Restrict all code to very simple control flow constructs — do not use goto statements, setjmp or longjmp constructs, or direct or indirect recursion.

When you use weird constructs then your code becomes difficult to analyze and to predict. The generations that came out after goto was considered harmful did indeed avoid using it. We're at the stage where we're debating if continue is goto and thus should be banned.


Rule 2 — All loops must have fixed bounds. This prevents runaway code.

All loops must have a fixed upper-bound. It must be trivially possible for a checking tool to prove statically that a preset upper-bound on the number of iterations of a loop cannot be exceeded. If the loop-bound cannot be proven statically, the rule is considered violated.

✅ Example: Fixed-Bound for Loop in C#

public class LoopExample
{
public void PrintNumbers()
{
// Fixed upper bound: loop runs exactly 10 times
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Number: {i}");
}
}
}

❌ Example: Avoid Unbounded Loops

public void UnboundedLoop()
{
while (true)
{
// Risk: this loop has no end condition!
Console.WriteLine("This will run forever!");
}
}

Rule 3 — Avoid heap memory allocation after initialization.

In performance-sensitive or deterministic systems (e.g., games, embedded devices, real-time services), it’s important to avoid heap allocations whenever possible. The heap is managed by the garbage collector (GC), and frequent or unnecessary allocations can lead to GC pauses, unpredictable performance, or memory pressure.
public void ProcessData()
{
Span<int> buffer = stackalloc int[10]; // Allocated on stack
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = i * i;
}
}

Rule 4 — Restrict functions to a single printed page.

No function should be longer than what can be printed on a single sheet of paper in a standard reference format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function.

Why Functions Should Fit on One Page

This rule is based on two key ideas:

  1. Human readability: The human brain can only process a limited amount of logic at once. Functions that fit on a symbolic “page” (~50 lines or fewer) are easier to understand, maintain, and debug. Nobody wants to navigate a 1000-line function doing too many things.
  2. Simplicity and decoupling: Smaller functions tend to do less, work on smaller data units, and use simpler logic. This leads to more modular, testable, and maintainable code.

Even though the “one-page” limit is arbitrary, it works well in practice — not because it’s precise, but because it enforces clarity and discipline. Over time, many developers come to appreciate that adhering to this principle naturally leads to better code.


Rule 5 — Use a minimum of two runtime assertions per function.

The assertion density of the code should average to a minimum of two assertions per function. Assertions are used to check for anomalous conditions that should never happen in real-life executions. Assertions must always be side-effect free and should be defined as Boolean tests. When an assertion fails, an explicit recovery action must be taken, e.g., by returning an error condition to the caller of the function that executes the failing assertion. Any assertion for which a static checking tool can prove that it can never fail or never hold violates this rule. (I.e., it is not possible to satisfy the rule by adding unhelpful “assert(true)” statements.)

Assertions are more than simple checks — they validate assumptions about input, output, and behavior, especially preconditions, postconditions, and invariants. In languages like C or Go, assertions often return error codes, while in higher-level languages, they typically raise exceptions.

using System;
using System.Diagnostics;

public class Calculator
{
public int DivideAndAdd(int numerator, int denominator, int addend)
{
// 🧪 Assertion 1: Precondition — denominator must not be zero
Debug.Assert(denominator != 0, "Denominator must not be zero.");

int result = numerator / denominator;

// 🧪 Assertion 2: Postcondition — result should be a valid integer (not overflowed)
Debug.Assert(result >= int.MinValue && result <= int.MaxValue, "Result is out of valid int range.");

return result + addend;
}
}

Rule 6 — Restrict the scope of data to the smallest possible.

Data objects must be declared at the smallest possible level of scope.

In short, don’t use global variables. Keep your data hidden within the app and make it so that different parts of the code can’t interfere with each other.

You can hide your data in classes, modules, second-order functions, etc.

❌ Bad Practice: Overly Broad Scope

public class ReportGenerator
{
int summary; // Should be local
int temp; // Should be inside loop
string message; // Not needed at class level

public void GenerateReport(List<int> data)
{
if (data == null || data.Count == 0)
{
Console.WriteLine("No data to process.");
return;
}
summary = 0;
foreach (var number in data)
{
temp = number * 2;
summary += temp;
}
if (summary > 100)
{
message = "High volume";
}
else
{
message = "Normal volume";
}
Console.WriteLine($"Summary: {summary}");
Console.WriteLine(message);
}

✅ Good Practice — Narrow Data Scope

public class ReportGenerator
{
public void GenerateReport(List<int> data)
{
if (data == null || data.Count == 0)
{
Console.WriteLine("No data to process.");
return;
}

// Restrict scope: summary only exists here
int summary = 0;

foreach (var number in data)
{
// Restrict scope: temp is used only in this loop
int temp = number * 2;
summary += temp;
}

Console.WriteLine($"Summary: {summary}");

// Restrict scope: message is only needed here
string message = summary > 100 ? "High volume" : "Normal volume";
Console.WriteLine(message);
}
}

Rule 7 — Check the return value of all non-void functions, or cast to void to indicate the return value is useless.

The return value of non-void functions must be checked by each calling function, and the validity of parameters must be checked inside each function.

In C, errors are typically indicated by the return value of a function or by passing an error variable by reference. In contrast, most interpreted languages use exceptions to signal errors. Even PHP 7 has improved in this regard, though non-fatal warnings still appear in HTML, potentially breaking JSON output.

The key principle here is: allow errors to propagate until you can handle them appropriately, either by recovering or logging them. In languages that support exceptions, this is straightforward — just avoid catching exceptions prematurely and only handle them when you can manage the situation properly.

Another way to think about it is: don’t catch exceptions too early or silently discard them. Exceptions are intended to alert you to serious issues, and the right response is to report the error and fix the underlying bug. This is particularly relevant in web development, where an exception typically results in a 500 Internal Server Error without causing a complete breakdown of the front-end.

❌ Bad Practices

public bool IsFileValid(string filePath)
{
// Perform some validation
return true;
}

public void ExampleUsage()
{
// Ignoring return value
IsFileValid("somefile.txt");
}

Why it’s bad:
 Ignoring return values can lead to missed information or errors. In this case, if IsFileValid were to return false, you wouldn't be able to detect it, potentially leading to issues in your program that could have been easily avoided by checking the return value.

✅ Best Practices

public bool IsFileValid(string filePath)
{
// Perform validation
return File.Exists(filePath);
// Returns true if file exists, false otherwise
}

public void ExampleUsage()
{
bool isValid = IsFileValid("somefile.txt");

if (isValid)
{
Console.WriteLine("File is valid.");
}
else
{
Console.WriteLine("File is not valid.");
}
}

Why it’s best:
 Checking the return value ensures that your program reacts appropriately based on the outcome of a function. By handling the result of IsFileValid(), you prevent issues where a file might be missing or invalid, and you provide useful feedback to the user.


Rule 8 — Use the preprocessor only for header files and simple macros.

The use of the preprocessor must be limited to the inclusion of header files and simple macro definitions. Token pasting, variable argument lists (ellipses), and recursive macro calls are not allowed. All macros must expand into complete syntactic units. The use of conditional compilation directives is often also dubious, but cannot always be avoided. This means that there should rarely be justification for more than one or two conditional compilation directives even in large software development efforts, beyond the standard boilerplate that avoids multiple inclusion of the same header file. Each such use should be flagged by a tool-based checker and justified in the code.
// header.h
#ifndef HEADER_H
#define HEADER_H

#define PI 3.14159 // Simple macro definition
#define MAX_SIZE 1024 // Another simple macro

void do_something(void);

#endif // HEADER_H

Keep the stack as small as possible. The less coupling you have the less complexity you need to handle.


Rule 9 — Limit pointer use to a single dereference, and do not use function pointers.

The use of pointers should be restricted. Specifically, no more than one level of dereferencing is allowed. Pointer dereference operations may not be hidden in macro definitions or inside typedef declarations. Function pointers are not permitted.

When you open the gate for this kind of things, you open the gate to any kind of pointer madness. And as you know, when a gate is open then people will go through it. Hence this rule for C.

❌ Bad Practice: Multi-Level Pointers (Double Dereference)

void set_value(int **pptr) {
**pptr = 42; // ❌ Violates the rule: more than one level of dereferencing
}

✅ Correct Usage (Single-Level Dereference Only)

#include <stdio.h>
void print_value(int *ptr) {
if (ptr != NULL) {
printf("Value: %d\n", *ptr); // One level of dereferencing: *ptr
}
}

Why it’s good:

  • Only one level of pointer indirection (*ptr).
  • Clear and safe to read and understand.

Rule 10 — Compile with all possible warnings active; all warnings should then be addressed before release of the software.

All code must be compiled, from the first day of development, with allcompiler warnings enabled at the compiler’s most pedantic setting. All code must compile with these setting without any warnings. All code must be checked daily with at least one, but preferably more than one, state-of-the-art static source code analyzer and should pass the analyses with zero warnings.

All compiler and static analysis warnings should be treated as errors and resolved before software is released. Warnings often indicate code that is bug-prone, non-portable, or undefined, and ignoring them can lead to critical runtime issues. We should check all of them.

gcc -Wall -Wextra -Wpedantic -Werror main.c -o myapp

🔍 Static Analysis Tools to Enforce This Rule

  • SonarQube / SonarCloud — Detects code smells, bugs, and security vulnerabilities
  • Cppcheck — Static code analysis for C/C++
  • Clang-Tidy — Linting tool for C/C++ based on Clang
  • Coverity Scan — Advanced static analysis for bug detection
  • PVS-Studio — Commercial-grade static analyzer

Making a conclusion

👨‍👦‍👦 Leave a comment, I am free for discussion with your any kind technical question.

#SafetyCriticalSoftware #CodingStandards #NASA #SoftwareEngineering #BestPractices #CProgramming #EmbeddedSystems #CodeQuality #SoftwareReliability #TechWriting

Version 1.0.1