Principle of Least Astonishment: Build Predictable Software
Table of contents
You call a function named getUser, and somewhere deep inside it writes a row to the database. Nothing in the name told you that. You reviewed the code, you shipped it, and weeks later you are staring at duplicate records, wondering how a read became a write. The function did exactly what its author intended. It just did something its name promised it would not, and everyone who touched that code after paid a small tax for the gap between what it said and what it did.
That gap has a name: the principle of least astonishment. It says that a component should behave in the way the people using it already expect, and that when a system surprises them, the surprise itself is a defect worth fixing. It is one of the oldest ideas in software design and one of the easiest to nod along to and then violate by lunchtime, because the behavior that astonishes your users is almost never astonishing to you. You wrote it. You hold the context. The whole difficulty of the principle is that it asks you to judge your own work by a standard you cannot feel from the inside.
Here is what the principle of least astonishment actually says, why a surprising system is more expensive than a merely limited one, where astonishment tends to come from, a checklist for catching it before it ships, and the one case where breaking an expectation on purpose is the right call.
What is the principle of least astonishment?
The principle of least astonishment, also called the principle of least surprise (POLA or POLS for short), states that a system, interface, or component should behave in a way that least surprises the people who use it. When someone reads a function name, clicks a button, or passes an argument, the result should match the expectation that name, button, or argument sets up. If the behavior and the expectation disagree, you change the behavior, not the user.
The idea predates the phrase. It runs through the C2 wiki's early design discussions, is catalogued in its UI and language forms on Wikipedia, and lives in the Unix tradition of small tools that do the obvious thing. It was sharpened over decades of people discovering, one bug at a time, that a clever surprise costs more than the cleverness is worth. The modern framing adds a useful corollary: if a feature genuinely has to carry a high "astonishment factor," because the surprising behavior is the point, then the feature itself is a candidate for redesign, not just its documentation.
What the principle is not is a demand for blandness. It does not say never do anything new. It says that when you depart from what people expect, the departure should be deliberate, visible, and worth it, and that most departures are none of those things. They are accidents of how the code grew, and they read to everyone downstream as a system that cannot be trusted to mean what it says.
Why surprising behavior is expensive
A limited system and a surprising system fail in different ways, and the second is worse. A limited system does less than you want, and you find out at the boundary: you reach for a feature, it is not there, you work around it. The cost is visible and it is paid once. A surprising system does something other than what you expect, and you find out later, through a bug, in production, after you have already built on the wrong assumption. The cost is hidden and it compounds.
Astonishment also erodes the thing that makes a codebase fast to work in: the ability to predict a component's behavior without reading it. Most of the time nobody reads the function. They read the name, form an expectation, and move on, because reading every function would make any real system unworkable. That shortcut is the entire economy of a large codebase, and a single surprising function devalues it everywhere, because now the shortcut is unreliable and people have to stop and check. A system that keeps its promises lets people reason about it from the outside. A system that surprises them forces everyone back inside.
There is a second-order cost, too. Once a surprising behavior has shipped and people have worked around it, the workaround becomes load-bearing. Someone depends on the surprise. Fixing it later breaks them, which is Hyrum's law arriving on schedule: with enough users, every observable behavior of your system, including the astonishing ones, ends up depended on by someone. The cheapest moment to remove a surprise is before it has any users. Every moment after that is more expensive than the last.
Where astonishment comes from
Surprises are not random. They cluster around a few recurring sources, and knowing them is most of the work of avoiding them.
Names that promise one thing and do another. A getX that also mutates, a validate that also saves, a boolean parameter that inverts the obvious reading. The name is the contract most people actually read, and when the body contradicts it, the name is lying. This is the same discipline that makes self-documenting code work: a name that matches its behavior is a name you do not have to verify.
Hidden side effects. A function that quietly writes to disk, mutates its argument, changes global state, or makes a network call that its signature gives no hint of. The classic example is a method that looks like a pure query and is secretly a command. The caller composes it as if it were free of consequences, and the consequences arrive anyway. Armin Ronacher's essay on Python and the principle walks through a language's worth of these, where a convenience in one place becomes a surprise in another.
Inconsistency with the rest of the system. The one endpoint that returns null where every other endpoint returns an empty list. The one config flag whose default is true where every sibling defaults to false. Each individual choice may be defensible, but the inconsistency is the surprise: people generalize from the pattern, and the exception breaks the generalization.
Breaking a convention the user brought with them. A text field where Ctrl+Z does nothing. A "Save" button that discards unsaved changes in another panel. A delete that cannot be undone where every other delete can. The user's expectation here did not come from your system at all. It came from every other system they have ever used, and you are measured against that whole history whether you like it or not.
Most of these come down to the curse of knowledge: you know what the code does, so the surprise is invisible to you. The behavior that astonishes a new reader is the behavior you have stopped seeing. This is why astonishment is so hard to self-review and so easy to catch in someone else's work, and why the most reliable detector of a surprise is a second person reading the thing cold.
Spotting astonishment: a checklist
You cannot feel your own surprises, but you can interrogate your work with questions that surface them. Before shipping an interface, a function, or a screen, run it against these:
- Does the name predict the behavior? If someone read only the name and never the body, what would they expect, and is that what happens? Any gap is astonishment.
- Are there side effects the signature does not reveal? Writes, mutations, network calls, global state changes. If the type says "query" and the body says "command," you have a surprise.
- Is this consistent with its siblings? Does the one method, endpoint, flag, or screen behave like the others in its family, or is it the exception people will trip on?
- Does it honor the conventions users arrive with? Standard shortcuts, standard button meanings, standard affordances. Breaking one of these costs more than inventing a new one, because you are fighting a lifetime of habit.
- When it fails, does it fail the way people expect? Surprising error behavior is still surprising behavior. A function that swallows an error and returns a plausible-looking default astonishes worse than one that throws.
- If you have to explain it, is the explanation short? "It does what it says" needs no explanation. A behavior that requires a paragraph of caveats in the docs is telling you the behavior, not the docs, is the problem.
The point of the list is not to pass it perfectly. It is to move the surprise from production, where a user finds it, to review, where you do.
Astonishing behavior and its least-astonishing fix
The abstract principle gets concrete fast when you line up the surprise against the version that keeps its promise. These are the patterns that recur:
| Astonishing behavior | Why it surprises | Least-astonishing version |
|---|---|---|
getUser() also creates a user if none exists | A "get" silently writes | Split into getUser() and getOrCreateUser(); the name states the write |
list.sort() returns None and mutates in place | Reads like it returns the sorted result | Offer both: sort() in place and sorted() returning a copy |
Boolean arg process(data, true) | The call site cannot say what true means | Named options: process(data, validate=True) |
| "Save" discards changes in an unfocused panel | Save should never lose work | Save everything, or disable Save until the panel is reconciled |
| Delete with no undo, next to deletes that have one | Inconsistent with its siblings | Match the family: offer undo, or clearly mark the difference |
| Config flag defaults opposite to every other flag | Users generalize from the pattern | Default it like its siblings, or rename so the difference is explicit |
| A method swallows errors and returns a default | Hides failure the caller needed to see | Surface the error; let the caller decide the fallback |
None of the fixes are clever. That is the point. The least-astonishing version is usually the more obvious one, and the surprise crept in because a shortcut, an optimization, or a late requirement made the obvious version slightly less convenient to write. The principle asks you to pay that small cost at the keyboard so that nobody pays the larger one downstream.
When breaking expectations is the right call
The principle has a sharp edge, and ignoring it turns least astonishment into an argument against ever improving anything. Sometimes the expectation people bring is wrong, or outdated, or the thing you are replacing was itself a bad convention. Breaking it is then the right move, and the principle still applies: it just changes what you owe the user.
What you owe is a deliberate, visible, well-signposted departure. If a new interaction is genuinely better, you make the new behavior discoverable, you give people a bridge from the old expectation, and you accept that the first encounter will cost a moment of surprise in exchange for a lasting gain. That is a different thing from an accidental surprise buried in a function name. It is the astonishment-factor corollary in action: the feature carries unavoidable surprise, so you redesign around the surprise, make it legible, and earn it, rather than letting it leak out as a bug.
The failure mode to avoid is using "sometimes you should break expectations" as cover for surprises you never decided to create. A deliberate departure can be named, defended, and pointed at in a design doc. An accidental one is only ever discovered. If you cannot say out loud why the surprise is worth it, it is not a bold design choice. It is a defect that has not been reported yet.
How least astonishment relates to its neighbors
The principle sits in a small family of ideas about building systems that other people can reason about. Postel's law, the robustness principle, is a close cousin pointed outward: be strict in what you emit and liberal in what you accept, so the systems on the other side of an interface are not astonished by your output. Least astonishment points the same instinct inward, at the people reading and calling your code. Conway's law explains why a system's surprises so often track its org chart: an interface designed by two teams that never talked will astonish anyone who assumed one mind was behind it. Read together, they describe the same goal from different angles, which is a system whose behavior you can predict without having built it.
For a notes app, the stakes are small but constant. A search that returns nothing when a note plainly contains the word, a sync that silently drops a change, an export that reorders your entries: each is a little astonishment, and a tool you reach for every day cannot afford to surprise you, because the trust it needs is built entirely out of behaving the way you expected last time.
Getting started
The principle of least astonishment is less a rule you apply than a question you keep asking: would this surprise someone who did not build it? You cannot answer it from the inside, so the practical move is to make the question routine, in code review, in design, and in the small daily act of naming things so the name tells the truth.
Scribelet is built to behave the way you expect. Your notes stay where you put them, search finds what is there, and your AI provider is the one you chose, with your key, under your control. If you want a thinking space that does not astonish you, try Scribelet free.
Share this article