Building a Unit Converter in JavaScript — The Edge Cases Nobody Talks About

javascript dev.to

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];
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 {};
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Source: dev.to

arrow_back Back to Tutorials