A unit converter seems like a simple project. Multiply by a conversion factor, display the result. But real unit converters have interesting edge cases that make them trickier to get right than they first appear.
Here's how to build a robust one, including the parts that trip people up.
The Basic Structure
The naive approach hardcodes conversion factors:
const conversions = {
'km-to-miles': 0.621371,
'miles-to-km': 1.60934,
'kg-to-lbs': 2.20462,
'lbs-to-kg': 0.453592,
};
function convert(value, from, to) {
const key = `${from}-to-${to}`;
return value * conversions[key];
}
This works but doesn't scale. Adding 10 units to a category means adding 90 conversion pairs (10 × 9 directional combinations).
The Base Unit Approach
Better: convert everything to a common base unit, then from the base unit to the target.
// All values relative to a base unit (meters for length)
const lengthUnits = {
mm: 0.001, // 1mm = 0.001m
cm: 0.01,
m: 1, // Base unit
km: 1000,
inch: 0.0254,
foot: 0.3048,
yard: 0.9144,
mile: 1609.344,
nmi: 1852, // Nautical mile
};
function convertLength(value, from, to) {
// Convert to base unit (meters), then to target
const valueInMeters = value * lengthUnits[from];
return valueInMeters / lengthUnits[to];
}
// Now adding a new unit only requires one entry
This approach reduces N² pairs to N values. It also makes the relationships explicit and auditable.
The Temperature Problem
Temperature is the exception that breaks the base-unit pattern. Unlike length or mass, temperature scales have different zero points. You can't just multiply.
function convertTemperature(value, from, to) {
// First convert to Celsius (base unit)
let celsius;
switch (from) {
case 'celsius':
celsius = value;
break;
case 'fahrenheit':
celsius = (value - 32) * (5 / 9);
break;
case 'kelvin':
celsius = value - 273.15;
break;
case 'rankine':
celsius = (value - 491.67) * (5 / 9);
break;
}
// Then convert from Celsius to target
switch (to) {
case 'celsius':
return celsius;
case 'fahrenheit':
return celsius * (9 / 5) + 32;
case 'kelvin':
return celsius + 273.15;
case 'rankine':
return (celsius + 273.15) * (9 / 5);
}
}
Temperature needs special handling for any unit system where zero doesn't mean "none of the thing being measured."
Floating-Point Precision
Try this in your browser console:
0.1 + 0.2 // → 0.30000000000000004
Unit conversions amplify this problem:
// User expects to see "1" after converting 1 meter to meters
convertLength(1, 'm', 'm') // → 1 (fine)
convertLength(1000, 'mm', 'm') // → 0.9999999999999999 (not fine)
The fix is careful rounding. But how much?
function convertWithPrecision(value, from, to, category) {
const result = performConversion(value, from, to, category);
// Determine significant figures to display
const inputSigFigs = significantFigures(value);
// Round to appropriate precision
return roundToSignificantFigures(result, Math.max(inputSigFigs, 6));
}
function significantFigures(n) {
if (n === 0) return 1;
const str = Math.abs(n).toString();
// Remove leading zeros and decimal point
const digits = str.replace(/^0+\.?0*/, '').replace('.', '');
return digits.length;
}
function roundToSignificantFigures(n, sigFigs) {
if (n === 0) return 0;
const magnitude = Math.floor(Math.log10(Math.abs(n)));
const factor = Math.pow(10, sigFigs - 1 - magnitude);
return Math.round(n * factor) / factor;
}
Units With Multiple Standards
"A cup" seems unambiguous. It isn't.
const volumeUnits = {
// US customary
us_cup: 0.000236588, // 236.588 ml
us_fl_oz: 0.0000295735,
us_pint: 0.000473176,
us_quart: 0.000946353,
us_gallon: 0.00378541,
// Imperial (UK)
uk_cup: 0.000284131, // 284.131 ml — different from US
uk_fl_oz: 0.0000284131,
uk_pint: 0.000568261, // Different from US pint
uk_gallon: 0.00454609, // Different from US gallon
// Metric
ml: 0.000001,
l: 0.001,
// Cooking measures (US)
teaspoon: 0.00000492892,
tablespoon: 0.0000147868,
};
A US gallon and a UK gallon are not the same. Neither are US and UK fluid ounces, cups, or pints. A converter that doesn't distinguish between these is wrong for anyone using it for precise applications.
Handling User Input Robustly
Users don't always type valid numbers:
function parseInput(input) {
// Handle empty input
if (!input || input.trim() === '') return null;
// Remove commas (e.g., "1,000" → "1000")
const cleaned = input.replace(/,/g, '').trim();
// Parse
const value = parseFloat(cleaned);
// Check for invalid results
if (isNaN(value)) return null;
if (!isFinite(value)) return null;
return value;
}
Also handle extreme values gracefully:
function convertSafely(value, from, to, category) {
if (value === null || value === undefined) return null;
// Check for physically impossible values
if (category === 'temperature') {
const celsius = toCelsius(value, from);
if (celsius < -273.15) {
throw new Error('Below absolute zero');
}
}
if (category === 'length' || category === 'mass') {
if (value < 0) {
throw new Error('Negative lengths and masses are not physically meaningful');
}
}
return performConversion(value, from, to, category);
}
Displaying Results
The output format should match the magnitude of the result:
function formatResult(value) {
if (value === null) return '';
const abs = Math.abs(value);
if (abs === 0) return '0';
// Use scientific notation for very large or small values
if (abs >= 1e12 || (abs < 1e-6 && abs > 0)) {
return value.toExponential(6);
}
// Determine decimal places based on magnitude
if (abs >= 100) return value.toFixed(2);
if (abs >= 10) return value.toFixed(3);
if (abs >= 1) return value.toFixed(4);
if (abs >= 0.1) return value.toFixed(5);
return value.toFixed(6);
}
Building the UI
The conversion should update in real-time on either input:
class UnitConverter {
constructor(category, units) {
this.category = category;
this.units = units;
this.lastChanged = 'from'; // Track which input was last edited
}
handleFromInput(value, fromUnit, toUnit) {
this.lastChanged = 'from';
const parsed = parseInput(value);
if (parsed === null) return { to: '' };
const result = convertSafely(parsed, fromUnit, toUnit, this.category);
return { to: formatResult(result) };
}
handleToInput(value, fromUnit, toUnit) {
this.lastChanged = 'to';
const parsed = parseInput(value);
if (parsed === null) return { from: '' };
// Reverse conversion
const result = convertSafely(parsed, toUnit, fromUnit, this.category);
return { from: formatResult(result) };
}
handleUnitChange(newFromUnit, newToUnit, currentValues) {
// When units change, update based on which was last edited
if (this.lastChanged === 'from' && currentValues.from !== '') {
return this.handleFromInput(currentValues.from, newFromUnit, newToUnit);
} else if (currentValues.to !== '') {
return this.handleToInput(currentValues.to, newFromUnit, newToUnit);
}
return {};
}
}
Try It
ToolZip's unit converter covers length, weight, temperature, area, volume, speed, and more — all processing in the browser.
toolzip.app/tools/unit-converter
ToolZip — 48 free browser-based tools. Everything runs client-side.