When code is a mess, developers call it spaghetti-like. But what exactly is spaghetti?
In Code Smell #2, planned as the next article, we continue with the following 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 noodles, making it impossible to tell where it will jump when you try to follow the order by eye.
The process of separating code whose control flow and responsibilities are tangled in one file continues in A case study applying separation of concerns to an 800-line component.
Following 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 piece in the ACM (Association for Computing Machinery) journal (Original article).
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 changed by the editor at the time, Niklaus Wirth.
That title format later solidified into the expression “○○ Considered Harmful,” so the editor’s influence traveled quite far.
The core argument was this: people read static code and construct the dynamic execution process in their minds.
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 sequential execution, conditional branching, and loops is different.
You can describe the execution location like coordinates: “the third iteration of the outer loop, in the true branch of the inner if.”
goto destroys this coordinate system.
This claim also had theoretical support. 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 therefore guaranteed that it was possible even without goto.
The Picture Created by goto
Looking at what code looked like then makes the idea clear.
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 with ten lines, you had to draw the flow on paper to understand it. Each GOTO was a line, and those lines crossed above and below.
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 spaghetti was the name given to those tangled masses.
goto Disappeared, but Spaghetti Remained
Something strange happens here. Modern code contains almost no goto.
Swift does not have it at all. In languages that do, its use has narrowed mostly to idioms for returning to resource-cleanup points.
Yet the term spaghetti code is used even more often.
goto was only one symptom, not the cause.
The real problem is that the reader cannot predict the flow, and modern code offers plenty of other ways to create that problem.
Nested hell. An if inside an if, a closure inside that, and another if inside the closure.
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 for each combination of conditions is just as difficult.
Callback hell. Chaining asynchronous work through callbacks causes execution order and code order to diverge.
The moment reading from top to bottom stops matching 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 should error handling go?
}
}
}
}
Global state. If any function can change a variable at any time, you must search the entire project to determine why its value became what it is.
That is also why singletons are frequently criticized.
Event soup. A sends a notification, B receives it and changes state, then C, which was observing that state, sends another notification.
Each piece is short and clean, but the overall flow is written down nowhere.
It is common in reactive code and, in some ways, is harder to trace than goto because the jump destination is not written in the code.
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, then 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 one, roughly speaking.
A common convention treats a value above 10 as a sign that it is time to split the function. McCabe himself presented the number as a reasonable upper bound, not an absolute rule.
A function that lists 20 cases with a single switch can exceed a complexity of 20 while remaining easy to read.
SwiftLint’s cyclomatic_complexity rule measures a similar value.
SwiftLint counts only if, guard, for, while, repeat, and case, catch, but not && or ??. Its default warning threshold is 10.
Enable it in a project, and a function that has been quietly growing will eventually be flagged.
Knuth’s Counterargument
Ending the story with “Dijkstra won” means you know only half of it.
In 1974, Donald Knuth argued the other side in a long 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.
Trying to eliminate it by adding a Boolean flag and more conditionals can produce worse code.
Today’s languages have settled on something close to a compromise: unlimited jumps are gone, but common patterns have dedicated syntax.
break, continue, labeled break label, and Swift’s defer and guard are examples.
// guard: Remove exceptional cases from the top and keep the main 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 C-era goto cleanup pattern 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 whose control flow cannot be predicted.
- Dijkstra’s 1968 letter was the starting point, and its central argument was that “you should be able to describe the execution location as coordinates.”
gotodisappeared, but deep nesting, nested callbacks, global state, and event chains recreate the same problem.- It can be roughly measured with cyclomatic complexity, with around 10 serving as a common warning line.
- As Knuth argued, the goal is not
gotoeradication, but predictable flow.guardandasync/awaitare mechanisms through which the language helps preserve that goal.
In the next article, we examine code broken in the opposite direction. Its flow is perfectly orderly, but adding one value requires changing seven files.
Lasagna code.
Continue reading
Sources and verification
- Go To Statement Considered HarmfulEdsger W. Dijkstra Archive · Original author source · Checked August 17, 2026Supports: The 1968 criticism of goto and the problem of inferring control flow
- Flow Diagrams, Turing Machines and LanguagesCorrado Böhm·Giuseppe Jacopini · Research paper · Checked August 17, 2026Supports: The expressiveness of sequential, selection, and iteration structures in 1966
- A Complexity MeasureThomas J. McCabe · Research paper · Checked August 17, 2026Supports: The 1976 definition of cyclomatic complexity and control-flow graphs
- Structured Programming with go to StatementsDonald E. Knuth · Research paper · Checked August 17, 2026Supports: Structured programming and the limited role of goto in 1974

![Cover image for [Code Smell #1] Why Is Spaghetti Code Spaghetti?](/assets/images/posts/a1c72f38-347f-4ee2-8e53-9a021038d44f/spaghetti-code-1.jpg)