Testing & Code Quality

[Code Smell #1] Why Is Spaghetti Code Spaghetti?

Spaghetti code refers not to length or messiness, but to control flow. This article traces the term from Dijkstra’s 1968 letter against goto, explains why spaghetti remained after goto disappeared, and shows how to measure it with cyclomatic complexity.

6 min read
Cover image for [Code Smell #1] Why Is Spaghetti Code Spaghetti?

When code is a mess, developers call it spaghetti. But what exactly is spaghetti?

In the upcoming Code Smell #2, we continue with the next package.

Because it is long? Because it is messy?

Neither. Spaghetti refers to control flow.

It originally meant a state where execution is tangled like strands of pasta, making it impossible to tell where control will jump next.

The process of separating code whose control flow and responsibilities are tangled in one file continues in An example of applying separation of concerns to an 800-line component.

Tracing the roots of the term leads to a letter published in 1968.

A Letter Written by Dijkstra

In March 1968, Edsger Dijkstra published a short article in the ACM journal (Original text).

Its title was “Go To Statement Considered Harmful.” It was a two-page letter.

Dijkstra’s original title was “A Case Against the Go To Statement.” The provocative title used today was added by the editor at the time, Niklaus Wirth.

That title format later became the idiom “○○ Considered Harmful,” so the editor’s influence traveled quite far.

The core argument was this: people read static code and construct a dynamic execution process in their heads.

But in code with many goto, knowing only that “this line is executing now” cannot explain the program’s state. You do not know where execution came from.

Code using only sequence, conditional branching, and iteration is different.

You can describe the execution position like coordinates: “the third iteration of the outer loop, in the true branch of the inner if.”

goto destroys this coordinate system.

There was theoretical support for this argument. Two years earlier, in 1966, Corrado Böhm and Giuseppe Jacopini proved that any program can be expressed using only sequence, selection, and iteration (Paper).

Mathematics had established that it was possible even without goto.

The Picture Created by goto

Looking at what code looked like then makes the idea intuitive.

10 IF X > 100 THEN GOTO 70
20 IF X < 0 THEN GOTO 90
30 Y = X * 2
40 IF Y > 50 THEN GOTO 70
50 PRINT Y
60 GOTO 100
70 PRINT "TOO BIG"
80 GOTO 100
90 PRINT "NEGATIVE"
100 END

Even a ten-line program required drawing the flow on paper to understand it. Each GOTO was a line, and those lines crossed vertically and horizontally.

It is not hard to imagine what happens when there are 500 lines.

That was common in BASIC and early Fortran code in the 1970s and 1980s, and the resulting clumps were called spaghetti.

Comparison of a flowchart tangled with GOTO jumps and a structured flowchart using only sequence and branching
On the left, the execution position can be described as coordinates; on the right, it cannot

goto Disappeared, but Spaghetti Remained

This is where things get strange. Modern code contains almost no goto.

Swift does not have it at all. In languages that do, its use has narrowed largely to idioms that return to resource-cleanup points.

Yet the phrase spaghetti code is used even more often.

goto was only one cause, not the symptom itself.

The real problem is that the reader cannot predict the flow, and modern code has plenty of other ways to create that problem.

Nesting hell. An if inside an if, then a closure, then another if.

When indentation deepens like an arrow, it is called the pyramid of doom. It does not jump around like goto, but tracing which line is reached under each combination of conditions is just as difficult.

Callback hell. Chaining asynchronous work through callbacks makes execution order diverge from code order.

The moment reading from top to bottom no longer matches execution order, the coordinate system collapses again.

// Execution order cannot be read as code order
loadUser(id) { user in
    loadProfile(user) { profile in
        loadPosts(profile) { posts in
            DispatchQueue.main.async {
                self.render(posts)   // Where does error handling belong?
            }
        }
    }
}

Global state. If any function can change a variable at any time, you must search the entire project to trace why its value became what it is.

That is also why singletons are often criticized.

Event soup. A sends a notification, B receives it and changes state, and C, which was observing that state, sends another notification.

Each piece is short and clean, but the overall flow is written down nowhere.

This is common in reactive code and, in some ways, is harder to trace than goto. The destination of the jump is not written in the code.

Image of a control-flow graph showing independent paths in cyclomatic complexity and a threshold gauge
The complexity number is not the answer; it signals where to look

Can It Be Measured with a Number?

“This code feels like spaghetti” is subjective. So in 1976, Thomas McCabe introduced a metric called Cyclomatic Complexity (Paper).

The calculation is simple: draw the code’s control flow as a graph and count the independent paths.

In practice, it is approximated by adding one to the number of branch points. Each if, for, while, case, &&, and ?? adds roughly one.

A common rule of thumb says that once the value exceeds 10, it is time to split the function. McCabe himself presented the number as a reasonable upper bound, not an absolute standard.

In practice, a function that lists 20 cases with a single switch exceeds a complexity of 20 but is easy to read.

SwiftLint’s cyclomatic_complexity rule measures a similar value.

SwiftLint counts only if, guard, for, while, repeat, and case·catch, not && or ??. Its default warning threshold is 10.

Enable it in a project and a quietly growing function will eventually be flagged.

Knuth’s Counterargument

Ending this story with “Dijkstra won” means knowing only half of it.

In 1974, Donald Knuth countered with a substantial paper titled “Structured Programming with go to Statements” (Paper).

His point was that goto itself is not evil. There are situations, such as exiting nested loops at once, where using goto actually makes the flow clearer.

If you try to eliminate it by adding a Boolean flag and more conditionals, the result is worse code.

Modern languages have settled on a compromise: remove unrestricted jumps, but provide dedicated syntax for common patterns.

break, continue, labeled break label, and Swift’s defer and guard are examples.

// guard: Remove exceptional cases from the top and keep the body flat
func process(_ data: Data?) throws -> Packet {
    guard let data else { throw ParseError.empty }
    guard data.count > headerSize else { throw ParseError.tooShort }
    // From here on, all conditions have been handled
    return try decode(data)
}

guard is essentially the goto cleanup pattern from the C era, established as a language feature. It fixes the jump destination to one place and gives it a name.

Summary

  • Spaghetti code is not messy code; it is code where the control flow cannot be predicted.
  • Dijkstra’s 1968 letter was the starting point, with the central argument that “you must be able to describe the execution position as coordinates.”
  • goto disappeared, but deep nesting, nested callbacks, global state, and event chains recreate the same problem.
  • Cyclomatic complexity provides a rough measure, with around 10 being a common warning line.
  • As Knuth argued, the goal is not to eradicate goto, but to make flow predictable. guard and async/await are language mechanisms that help enforce that goal.

The next article looks at code broken in the opposite direction. The flow is perfectly neat, but adding one value requires changing seven files.

Lasagna code.

Continue reading

Sources and verification