Have you ever encountered a function like this during a code review?
It was named getUser(), but inside it looked up a user, updated the last-login time, refreshed the cache, and even sent a notification email if the account was dormant.
If you called it inside a loop thinking, “It’s just a lookup, so it should be safe,” based only on the name, wouldn’t you end up sending an email bomb to your users?
The principle we’re covering today is the Principle of Least Astonishment, which helps catch code like this. It’s part of our series on development principles covering KISS, DRY, YAGNI, and SOLID—and today’s principle has an especially fun name.
The Principle of Least Astonishment, or POLA, says this:
Code should behave the way its readers expect. If a design surprises its users, rethink the design.
It’s an old principle from system design in the 1960s and 1970s, but it applies broadly to everything from UI and API design to everyday code.
Why Is Astonishment Costly?
In programming, “astonishment” is not an emotional issue; it’s a cost issue.
When developers read code, they form hypotheses based on names and conventions. getUser probably only performs a lookup, and isValid probably returns a Boolean. When the code behaves as expected, it reads smoothly.
But the moment a hypothesis breaks, you have to stop reading, jump into the function, and verify what it really does. Even with only ten such functions in a codebase, distrust sets in: “I can’t trust the names in this code.” From then on, you open every function as you read it. Your reading speed is cut in half.
The worst case is using the code according to your hypothesis without checking and causing an incident—like the email bomb above.
Where Astonishment Appears in Code
Let me organize this around patterns I’ve encountered in practice.
1. Functions Whose Names Don’t Match Their Behavior
// The name says lookup, but it changes state
func getUser(id: Int) -> User {
let user = db.find(id)
user.lastSeenAt = Date() // Astonishment 1: Side effect
db.save(user)
if user.isDormant {
mailer.send(to: user.email, message: "Dormant-account reactivation notice") // Astonishment 2: External call
}
return user
}
Hidden writes in lookup functions are a classic source of astonishment. get should only read; if it changes state, use a name like update or touch.
2. Return Values That Betray Conventions
If one function returns nil when it can’t find something, another throws an exception, and another returns an empty object, callers have to gamble every time. It’s important to standardize on one approach.
3. Silent Failures
func parseConfig(_ json: Data) -> Config {
guard let config = try? JSONDecoder().decode(Config.self, from: json) else {
return Config() // Astonishment: The configuration is corrupted, but an empty configuration is returned as if nothing happened
}
return config
}
If a configuration file is corrupted and the system quietly falls back to defaults, users suffer later, asking, “Why didn’t my configuration apply?” Failures are less surprising when they are reported loudly.
How to Write Less Surprising Code
Here are the practices I try to follow.
| Practice | Details |
|---|---|
| A name is a contract | Do only what the name promises; if you do more, change the name |
| Follow conventions | Don’t depart from the existing style of the language, framework, or team without a reason |
| Make side effects explicit | Make it clear from the name when a function changes state |
| Document anything surprising | If unusual behavior is unavoidable, call it out prominently in comments and documentation |
The second practice is the most powerful. Across an entire team, boring standard approaches always beat clever personal ones. It’s exactly the same idea as in the previous KISS installment, where I said an ordinary loop is better than an “impressive one-liner.”
Wrapping Up
There are three things to remember about the Principle of Least Astonishment.
- Code should behave as expected based on its names and conventions. Code that violates expectations is a breeding ground for bugs.
- Don’t hide side effects in lookup functions, and don’t silently swallow failures.
- On a team, boring and predictable code beats clever code.
If I had to choose one criterion for good code, it would be this: code that doesn’t surprise its readers.
If you recently read some code and thought, “Huh? Why does this work that way?”, that was a moment when this principle was violated.

