Article
You know those moments when you think a task is going to take 20 minutes, and then you're still debugging edge cases four hours later? That was me with a base converter. I mean, how hard can it be? It's just parseInt() and .toString(), right?
Spoiler: it's never just that.
While working on a collection of browser-based developer tools, I needed a base converter that could handle binary, octal, decimal, and hexadecimal conversions in real-time. I thought I'd have it done before my coffee got cold. Instead, I learned more about JavaScript number limitations, BigInt quirks, and input validation than I ever wanted to know.
The "Simple" Problem That Wasn't
The core requirement sounded straightforward: build a tool where you type a number in any base and see it instantly converted to all other bases. Type ff in hexadecimal, and you should see 255 in decimal, 377 in octal, and 11111111 in binary — all updating as you type.
The first version took about 15 minutes. It was beautifully naive:
function convert(value, fromBase, toBase) {
return parseInt(value, fromBase).toString(toBase);
}
Three lines. What could possibly go wrong?
When JavaScript Numbers Betray You
The first bug appeared with numbers that should have worked fine. I typed 99999999999999999999 in decimal and watched the tool return complete garbage in binary. Not even close to correct — just nonsense.
Here's the thing: JavaScript numbers are IEEE 754 double-precision floating point. They can only precisely represent integers up to 2^53 - 1 (about 9 quadrillion). Anything larger loses precision silently. No error, no warning — just wrong answers.
The solution was BigInt, but it came with its own set of headaches. You can't just swap parseInt() for BigInt() and call it a day. The API is different, you need explicit conversion functions, and you have to handle the fact that BigInt doesn't work with Math functions or the toString() method in the same way.
After some iteration, I ended up with something like this:
function convertValue(value, fromBase, toBase) {
if (value === '') return '';
const bigIntValue = parseToBigInt(value, fromBase);
return bigIntValue.toString(toBase);
}
The key insight was creating a custom parseToBigInt function that handles the base validation and character checking before conversion. This way, I could catch invalid input early and show a friendly error instead of silently producing garbage.
The Validation Rabbit Hole
This is where the project really started to consume my day. Each base has its own set of valid characters:
- Binary: only
0and1 - Octal:
0through7 - Decimal:
0through9 - Hexadecimal:
0through9,athroughf(case-insensitive)
Seems simple, but users will type anything. I've seen people try to enter 2 in binary, 8 in octal, and once someone tried to enter g in hex — which is technically valid for base 17, but definitely not for base 16.
The validation logic ended up being the most important part of the whole tool. Without it, you get silent failures and confusing results. With it, you get a clear error message and a red border on the offending input.
function validateInput(value, base) {
const validChars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base);
const regex = new RegExp(`^[${validChars}]+$`, 'i');
return regex.test(value);
}
This single function prevented more confusion than everything else combined. The moment someone types an invalid character, they see exactly what's wrong instead of watching numbers transform into something unrecognizable.
The Custom Base Feature That Almost Got Cut
The requirements called for supporting custom bases from 2 to 36. I initially thought this would be trivial — just make the base a variable instead of a constant. But the implementation had a subtle complexity: the custom base input itself needed to be a number input, while the value input needed to be validated against that base.
The tricky part was handling the interaction between the two. If someone changes the base from 16 to 2 while there's a ff in the custom value field, what should happen? I decided to clear the custom value and show a message that the value is invalid for the new base. It's not the most elegant solution, but it's honest and doesn't pretend to do something it can't.
The Real-Time Conversion Architecture
The core challenge was making all conversions happen instantly without creating infinite loops or performance issues. Here's the pattern I settled on:
let isUpdating = false;
function handleInput(event) {
if (isUpdating) return;
isUpdating = true;
const sourceId = event.target.id;
const value = event.target.value;
const base = getBaseFromId(sourceId);
if (!validateInput(value, base)) {
markInvalid(event.target);
clearConversions();
isUpdating = false;
return;
}
const bigIntValue = parseToBigInt(value, base);
updateAllFields(bigIntValue, sourceId);
updateBitInfo(bigIntValue);
isUpdating = false;
}
The isUpdating flag was crucial. Without it, updating one field would trigger updates in others, which would trigger updates back, creating an infinite loop. This is a classic problem in any real-time synchronized form, and the boolean guard is the simplest solution.
Where AI Actually Helped (and Where It Didn't)
I used AI assistance for this project, and it was a mixed bag — which I think is the honest truth about AI-assisted development right now.
What AI handled well:
- Generating the initial HTML structure with all the input fields and buttons
- Writing the i18n mechanism for Chinese/English support
- Creating the responsive CSS with dark mode support
- Implementing the copy-to-clipboard functionality
The AI was particularly good at boilerplate and structure. I could describe what I wanted in plain language and get a functional skeleton in seconds.
Where AI struggled:
The first version of the conversion logic was completely broken for large numbers. The AI used parseInt() without any consideration for precision issues. When I pointed out the problem, it suggested using BigInt but then made a mess of the conversion functions, mixing BigInt and regular number operations in ways that threw type errors.
The validation logic also took several iterations. The AI initially used simple character checks that didn't properly account for case-insensitivity or the full range of valid characters in higher bases.
The lesson: AI is great for scaffolding and structure, but you still need to understand the domain deeply enough to catch its mistakes. In this case, the numerical edge cases and validation logic required genuine understanding of both JavaScript's limitations and the mathematical requirements of base conversion.
Performance Considerations
One thing that surprised me was how easy it was to make this performant. The conversions themselves are trivial — even with BigInt, converting a 100-digit number takes microseconds. The real performance concern was DOM updates.
The solution was to batch all DOM updates together and only update what actually changed. Instead of updating every field on every keystroke, I compare the new value with the current value and only update if they're different:
function updateField(id, value) {
const element = document.getElementById(id);
if (element.value !== value) {
element.value = value;
}
}
This tiny optimization prevents unnecessary DOM mutations and keeps the tool responsive even when pasting in a massive number.
The Bit Operation Feature
The requirements included showing bitwise operations (AND, OR, XOR, NOT, left shift, right shift). This was the feature I almost cut because it seemed unnecessary. But it turned out to be the most interesting part.
The challenge was that JavaScript's bitwise operators work on 32-bit integers, which meant I had to use BigInt for the bit operations too:
function showBitOperations(value) {
const bigInt = BigInt(value);
return {
not: ~bigInt,
leftShift1: bigInt << 1n,
rightShift1: bigInt >> 1n,
};
}
The n suffix on numeric literals for BigInt was a subtle gotcha that took me a minute to remember. Small things like that are where AI assistance actually helped — it caught the missing n before I did.
What I Learned
-
Never trust JavaScript number precision — always use BigInt for anything that could exceed
2^53 - Validation is the most important feature — a tool that gives wrong answers silently is worse than one that refuses to work
- The infinite loop guard is non-negotiable — any real-time synchronized form needs a reentrancy guard
- AI assistance is a multiplier, not a replacement — it made me 3x faster, but I still needed to understand every line it generated
The Result
After all that, I ended up with a tool that does exactly what I originally wanted: type a number in any base and see it instantly converted everywhere else, with bit operations and byte size displayed below. It's fast, handles huge numbers correctly, and gives clear errors when you type something invalid.
During this process, I built a small browser-based tool to make this workflow easier. If you're interested, you can check it out here.
The next time you use a base converter, spare a thought for the poor developer who had to handle BigInt edge cases and validation regexes. It's never as simple as it looks.
Tags
javascriptwebdevprogrammingtutorialtooling
Top comments (0)