Lesson 3.6 · 10 min read
This post is about four unglamorous facts. They are not architecture. They are not clever. They are properties of how machines store values, and they have been producing the same bugs, in the same ways, for fifty years.
They will still be producing them in fifty more, because they are not caused by bad code. They are caused by the gap between how humans think about numbers, absence, time, and text, and how a computer is obliged to represent them. No model writing your implementation removes that gap. It reproduces it, faithfully, because the tutorials it learned from contained it too.
Every experienced engineer has been burned by all four. Reading about them takes twenty minutes and will save you, at some point, an entire day.
Ask any programming language to add ten cents to twenty cents. In most of them, you will not get thirty cents. You will get something like 0.30000000000000004.
This is not a bug in the language. It is arithmetic working exactly as designed.
Computers store fractional numbers in a format called floating point, which represents values in binary, in base two. And some numbers that are perfectly clean in base ten cannot be written exactly in base two, in the same way that one third cannot be written exactly in base ten: 0.333... goes on forever, so you round. Binary has that problem with 0.1. It cannot be stored exactly, only approximately, and the tiny error is real.
Do arithmetic on approximations and errors accumulate. Add a thousand prices and the total may be off by a fraction of a cent. Compare two amounts that should be equal and find them unequal by a vanishing amount, which means your check for whether the invoice balances quietly fails.
The rule that follows is absolute, and it is the one thing to take from this section: never store money as a floating point number. Not in a variable, not in a database column, not in JSON.
Instead, store money as an integer, in the smallest unit. Not 19.99 dollars. 1999 cents. Integers are exact. There is no rounding, no accumulation, no drift. You divide by a hundred at the moment you display it to a human, and only then. Databases also offer an exact decimal type for this purpose, which is the right choice when you need fractional cents.
The insidious part is that a system storing money as floating point works perfectly during development, produces correct-looking numbers on every screen, and is wrong by a few cents across ten thousand transactions in a way that nobody notices until an accountant does. When an AI writes you a schema and gives price a floating point type, which it will, it has produced exactly what a thousand tutorials contain, and it is wrong, and you are the only one who will catch it.
A field is empty. What does that mean?
It might mean nobody has set it yet. It might mean someone deliberately cleared it. It might mean the value legitimately is zero, or an empty string. It might mean the data was never collected. It might mean an error occurred and nothing was written.
Programming languages give you a small vocabulary for absence, and the words are not the same. Depending on the language you'll have some combination of null, undefined, an empty string, and zero. They are different values. Code that checks for one does not catch the others, and the case that slips through is the one that breaks in production.
The classic failure is treating them as interchangeable. A check written to see whether a value is "missing" often accidentally treats zero as missing, because zero is falsy in many languages. So a user with zero credits is treated as having no credit information at all, and shown the wrong screen, and nobody understands why it only happens to people with exactly zero.
Databases have their own version, and it's stricter and stranger. NULL in SQL does not mean empty. It means unknown, and it behaves accordingly: NULL is not equal to NULL, because two unknown things cannot be said to be the same. Comparing anything to NULL yields neither true nor false. A WHERE clause filtering on a column will silently exclude rows where that column is NULL, even when you meant to include them, because the comparison was never true.
The discipline is to decide what absence means in your domain, and encode it deliberately. If a task has no due date, is that null, or is that a distinct state? If a user hasn't set a display name, do you store null, or an empty string, and does every piece of code agree? Where a value must always exist, say so in the schema, make the column not-nullable, and let the database refuse anything else. This is the constraint idea from post 3.4 again: a rule the database enforces cannot be violated by code that forgot.
Whenever you read code, and especially code you didn't write, look at what happens when the value isn't there. It is where a startling fraction of bugs live, and it is exactly the case nobody tests.
Time seems simple and is the single richest source of embarrassing bugs in all of software.
A timestamp with no timezone is meaningless. "The meeting is at 3pm" is not information; it is information plus an assumption about where you're standing. Store 3pm, read it in another country, and you have a wrong answer that looks exactly like a right one.
Timezones are political rather than geographical. They change. Governments alter them, sometimes with a few weeks' notice. Daylight saving shifts them twice a year, on different dates in different places, and some places don't observe it, and some observe it in the opposite half of the year. Offsets are not always whole hours.
Daylight saving produces genuinely broken moments. When clocks spring forward, a local time simply does not exist that day. When they fall back, an hour repeats, and a timestamp within it is ambiguous: it happened twice.
The practice that survives all of this is simple and should be treated as a rule.
Store every instant in UTC. Coordinated Universal Time has no daylight saving and no politics. It is the same everywhere. Every timestamp in your database, every timestamp in your logs, every timestamp in your API: UTC.
Convert to a local timezone only at the moment of display, in the interface, using the timezone of the person looking at it. Never store the converted value. Never do arithmetic on it.
And notice the exception, because it's the interesting one. A future appointment is not an instant. If a doctor's appointment is at 9am local time next March, and the government moves the timezone in February, the appointment is still at 9am, and the instant in UTC has changed. So for future events you must store the local time and the timezone identifier, not the UTC instant, because the instant is a derived value that can change. Getting this wrong is why calendar applications occasionally move all of your meetings by an hour.
A last, small thing that is worth knowing because it will confuse you exactly once. A date is not a timestamp. Someone's birthday is a date, the same everywhere. Store it as a date. Storing it as a timestamp with a timezone means it will shift by a day for people in some parts of the world, and someone will get a birthday email a day early, and you will spend an afternoon working out why.
Text seems like the simplest thing here and contains a subtlety that leaks into surprising places.
A computer stores numbers, so text is stored by assigning a number to each character. The problem is that there are far more characters in human writing than anyone anticipated, and the early schemes accommodated English and little else.
The modern answer is Unicode, which assigns a number to every character in every script, plus emoji, and UTF-8, the dominant way of writing those numbers as bytes. UTF-8 uses one byte for the characters that used to be everything, and more bytes for everything else. This is why it took over: existing English text was already valid UTF-8, and the rest of the world became expressible.
Which produces a fact that will eventually matter to you. The number of characters is not the number of bytes. A character in a non-Latin script may take several bytes. An emoji may take four, and some emoji are made of several code points glued together, so a single visible symbol may count as two, or seven, depending on what you ask.
So a database column limited to a hundred bytes does not hold a hundred characters. A validation checking a name's length is measuring one of three plausible things. Reversing a string of emoji does not do what you expect. Truncating a string at a byte boundary can cut a character in half and produce the little replacement diamond that shows up in badly-built software everywhere.
The practical guidance is short. Use UTF-8 everywhere, in your database, your files, your APIs, your responses, with no exceptions and no negotiation. Never assume one character is one byte. And when a name renders as question marks, or an emoji breaks a field, or a string is silently truncated into nonsense, you now know exactly which of your assumptions was wrong.
These are not advanced topics. They are the opposite: they sit underneath everything, and they are usually skipped because they're boring, and then they resurface as a bug that costs a day and a small amount of dignity.
Notice what they have in common. In every case, a human concept, an amount of money, a thing being absent, a moment in time, a piece of writing, does not map cleanly onto how a machine represents it. The gap is real and permanent. It is not a deficiency in any language, and no framework abstracts it away, because the gap is between the world and arithmetic.
Which is exactly why it survives the automation of code writing. An AI produces a schema with a floating point price column because that pattern appears everywhere in its training data, and it appears everywhere because people keep making this mistake. It stores a naive timestamp, because most code does. It writes a check that treats zero as missing, because that's idiomatic and usually harmless and occasionally catastrophic. The code will look correct. It will pass review. It will run.
Someone has to know. Someone has to look at a schema and say that price should be an integer of cents. Someone has to ask whether that timestamp is UTC, and whether that appointment is an instant or a local time. Someone has to notice that a user with zero of something is being treated as a user with nothing known about them.
That someone is not going to be the machine. It's going to be a person who read something like this once and never forgot, which is now you.
Four checks, fifteen minutes, on anything you've built or can look at.
Find where money is stored. Look at the column type. If it's a floating point type, you have found a real defect. Fix it: integer cents, or an exact decimal type, and divide only at the moment of display.
Find where a timestamp is stored, and answer whether it is UTC. Then find where it is displayed and check that the conversion happens there and only there. Then find any future-dated event in your system and ask whether it should be an instant or a local time with a timezone.
Find a check for a missing value, anywhere in your code. Ask what it does when the value is zero, or an empty string, or a genuine null. Try each. At least one of them will surprise you.
And put an emoji in a text field, then look at how it's stored, how long the system thinks it is, and what happens if you truncate it. It costs one minute, and it will tell you more about your system's assumptions about text than any amount of reading.
You are not learning trivia. You are learning the four places where a computer's honesty about its own limitations gets mistaken for a bug in your code, and knowing them is a permanent, quiet advantage over almost everyone building software today.