I built a volume converter. The interesting part is not the math — it is one line long. The interesting part is that a cup is not a cup.
A recipe said 1 cup. My converter said 236.59 mL. The recipe meant 250 mL. That is a 13.41 mL gap per cup, about 5.7% — invisible in a soup, real in a cake. The bug was not in the arithmetic. It was in the assumption that "cup" names one volume. It does not. So the converter I built treats units as locale-dependent labels, not as fixed numbers. Here is how the pieces fit together.
Every unit converts through one base
The cleanest design is a table of factors against a single base unit, and for volume that base is the milliliter. Add a unit and you add one line — no conversion matrix, no special cases that drift out of sync.
const ML = { ml: 1, l: 1000, // exact, by definition usCup: 236.5882365, // US customary cup metricCup: 250, // metric cup — a different unit usTbsp: 14.7867648, // exactly 3 US teaspoons usTsp: 4.92892159, usFlOz: 29.5735296, impFlOz: 28.4130625, // smaller than the US fl oz usPint: 473.176473, // 16 US fl oz impPint: 568.26125, // 20 Imperial fl oz usQuart: 946.352946, // 4 US cups usGal: 3785.411784, // US gallon impGal: 4546.09, // Imperial gallon — ~20% larger m3: 1_000_000, // 1 cubic meter = 1,000 liters }; function convert(value, from, to) { return (value * ML[from]) / ML[to]; } Two facts in that table surprise most people. First, the Imperial fluid ounce is smaller than the US fluid ounce (28.41 mL vs 29.57 mL), even though the Imperial gallon is larger than the US gallon (4.546 L vs 3.785 L). The two systems do not scale together, so any converter that treats "gallon" and "fluid ounce" as one family is wrong in both directions. Second, the metric side is exact by definition — 1 liter is 1,000 mL, 1 mL is 1 cubic centimeter — while the customary side is a pile of historical accidents with long decimal tails.
That is lesson one: the conversion factor is a definition, not a measurement. There is no sensor noise here. A converter has no excuse for being approximately right.
"Cup" needs a locale, not a number
The real design question is what the converter does when a user types "cup" and nothing else. Three options exist, and each is defensible:
- Default to the US customary cup (236.59 mL), because most English-language recipes are American.
- Default to the metric cup (250 mL), because it is the rounder number and the international standard.
- Refuse to guess, and ask which cup the recipe means.
My converter defaults to US customary — the tool's audience is mostly US recipes — but labels the unit explicitly as "US cup" so the assumption is visible, not silent. A silent default is how a cake ends up 5.7% short on liquid per cup. The same logic applies to gallons: the tool never shows a bare "gallon". It shows "US gallon" or "Imperial gallon", because a bare gallon is a coin flip worth 0.76 liters.
The same trap hides in tablespoons. A US tablespoon is 14.79 mL. An Australian tablespoon is 20 mL. Four Australian tablespoons in a small sauce is a quarter-cup error — enough to notice. If your converter hides which tablespoon it means, it is not converting. It is guessing.
Display rounding is a display decision
Convert 1 liter to US cups and back: 4.22675 cups, rounded to 4.23, back to 1.0008 liters. You just invented 0.8 mL. Do that across a recipe scaled four ways and the drift becomes real error. So the converter stores full precision and rounds only for display, with a precision selector the user controls:
function display(value, precision) { // The stored value never changes; only the digits on screen do. return Number(value.toFixed(precision)); } const cups = convert(1, "l", "usCup"); // 4.2267528377... display(cups, 2); // 4.23 on screen convert(display(cups, 2), "usCup", "l"); // 1.0008 — display loss, not storage loss convert(cups, "usCup", "l"); // 1.0 — full precision round-trips cleanly Round-trip error is the test I run on every unit pair: convert there and back at full precision, and the result must equal the input. If it does not, the bug is in the table, not in the rounding.
The one conversion a volume tool must refuse
Volume cannot become grams. A cup is a volume; a gram is a mass. The same cup holds a different weight of flour than of sugar, so the conversion needs the ingredient's density — information the number alone does not carry. My converter says so on the page instead of inventing an answer:
function cupsToGrams(cups, ingredient) { if (!ingredient || ingredient.density == null) { throw new Error("Volume needs a density to become mass."); } return cups * 236.5882365 * ingredient.density; // mL * g/mL } Commonly used approximations — 1 cup of all-purpose flour ≈ 120 g, sugar ≈ 200 g, butter ≈ 227 g — are averages, not definitions. A converter that bakes one of them in silently is not being helpful. It is hiding a guess behind a decimal point.
The debatable part
So here is the question I keep coming back to: when a recipe says "1 cup" and nothing else, should a converter default to the US cup, default to the metric cup, or refuse to guess and ask? I defaulted to US customary with an explicit label. A reasonable person could argue the metric cup is the better default — it is the international standard, and 250 is a rounder number. Another could argue that any silent default is a bug, and the tool should force the choice.
Which would you pick — and what is the worst "wrong cup" story you have? I built this converter, and I am still not sure the default is right. You can try it free, no sign-up, entirely in your browser: Volume Converter.
