Beyond 'eval()': How I Built a Step-by-Step Math Calculator That Explains Its Work with AI
Building a calculator that returns an answer takes little code. Building one that explains how it reached that answer creates a different problem. Take this expression: 12 + 6 × 3 JavaScript can calculate the result with simple code. const result = 12 + 6 * 3; console.log(result); // 30 That gives the correct answer. It does not explain the work. A student may need to see this: 12 + 6 × 3 Step 1: Multiply 6 by 3. 6 × 3 = 18 Step 2: Add 12 and 18. 12 + 18 = 30 Final answer: 30 That difference between calculating and explaining became the main challenge when I worked on a step-by-step math calculator. I wanted something that could handle more than basic arithmetic. The calculator needed to work with equations, fractions, algebra, percentages, calculus, word problems, and other math questions. I also wanted clean mathematical notation. That meant solving several separate problems: understand the user's input solve the problem produce useful steps format mathematical expressions render those expressions in the browser check the result where possible keep the API secure I ended up using an AI model together with normal application code, LaTeX, and MathJax. The AI handles the flexible reasoning. LaTeX describes the math. MathJax turns the LaTeX into readable mathematical notation. JavaScript handles the interface and application logic. Why eval() is not enough A basic JavaScript calculator often starts with something like this: const expression = "12 + 6 * 3"; const answer = eval(expression); console.log(answer); There are two problems with this approach. The first problem is security. Passing raw user input to eval() can execute JavaScript. That makes it a poor choice for a public calculator. The second problem matters even more here. eval() gives me this: 30 It does not give me this: 6 × 3 = 18 12 + 18 = 30 It also cannot explain why multiplication happens before addition. The limitation becomes clearer with algebra. Consider: 2x + 5 = 17 A useful result should look like this: 2x + 5 = 17 Subtract 5 from both sides. 2x = 12 Divide both sides by 2. x = 6 JavaScript does not understand 2x + 5 = 17 as a normal JavaScript expression. A step-by-step solver needs more than expression evaluation. Hard-coding every possible solution gets messy One option would be to write custom solving logic for every type of math. I could start with addition. Then subtraction. Then multiplication. Then division. Then fractions. Then percentages. Then linear equations. Then quadratic equations. Then powers and roots. Then logarithms. Then derivatives. Then integrals. The amount of code would keep growing. Even one category contains many different forms. A linear equation might look like this: 2x + 5 = 17 It might also look like this: 4(x - 2) = 20 Or: 3x + 7 = x + 19 The steps change for each case. Word problems create another issue. A rectangle has a length of 12 cm and a width of 7 cm. What is its area? A normal expression parser first needs to understand the sentence. It then needs to identify the correct formula. Only after that can it calculate the answer. I did not want to create thousands of explanation rules by hand. That is where an AI model became useful. Where AI fits I do not treat the AI model as the whole application. It handles the part that benefits from flexible reasoning and language. The rest of the system still uses normal code. A simplified flow looks like this: User enters a problem ↓ Application processes the input ↓ Solver receives the problem ↓ AI works through the solution ↓ AI returns structured output ↓ Application checks the response ↓ MathJax formats the math ↓ Steps appear in the browser The model can understand different forms of math without requiring one hard-coded path for every possible question. That does not mean the model gets full control. The application still decides how the response should look and how it should reach the screen. I ask for structured output I do not want the model to return a random block of text. A response like this is difficult to control: Okay! Let's solve this problem. First we need to... The wording may change on every request. The frontend also has to guess where one step ends and another begins. Structured output works better. For example: { "finalAnswer": "\\(x = 6\\)", "steps": [ { "explanation": "Start with the equation.", "math": "\\[2x + 5 = 17\\]" }, { "explanation": "Subtract 5 from both sides.", "math": "\\[2x = 12\\]" }, { "explanation": "Divide both sides by 2.", "math": "\\[x = 6\\]" } ] } Now the roles stay clear. The AI produces the solution data. The application controls the interface. This gives me much more predictable output. Why I use LaTeX Getting the correct steps from AI solves only part of the problem. Math can look bad in plain text. Take the quadratic formula. The AI could return: x = (-b +- sqrt(b^2 - 4ac)) / 2a A human can understand it. It does not look like proper mathematical notation. LaTeX gives the model a standard way to describe the expression. x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} The same approach works for fractions: \frac{3x + 2}{x - 5} Square roots: \sqrt{x^2 + 9} Powers: x^{12} Integrals: \int_0^5 x^2 \, dx Matrices: \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} LaTeX gives the application one consistent format for mathematical expressions. The AI does not need to invent a different visual style for every problem. AI gives me LaTeX. MathJax makes it readable. LaTeX is still text. A browser does not automatically turn this: \[\frac{x+2}{3}=7\] into a properly formatted equation. That is where MathJax comes in. The full flow looks more like this: User Input ↓ Input Processing ↓ Solver Router ↓ AI or Local Math Engine ↓ Validation ↓ Structured JSON ↓ LaTeX Expressions ↓ MathJax ↓ Step-by-Step Interface The AI decides what the solution should contain. LaTeX describes the mathematical notation. MathJax renders that notation inside the page. Each part has one clear job. I tell the model exactly how to format math The prompt matters a lot. I do not send only: Solve this: 2x + 5 = 17 That gives the model too much freedom. I define the expected output. A simplified prompt might look like this: Solve the math problem step by step. Keep each explanation short. Do not skip important operations. Return mathematical expressions as LaTeX. Use \( ... \) for inline math. Use \[ ... \] for equations that should appear on their own line. Do not return HTML. Do not place LaTeX inside Markdown code blocks. Return valid JSON. Use this structure: { "finalAnswer": "", "steps": [ { "explanation": "", "math": "" } ] } I can also add instructions for specific types of problems. For algebra: Show what operation happens to both sides of the equation. For fractions: Show the common denominator when one is required. For calculus: State the rule used before applying it. The prompt does not need to teach mathematics from scratch. The model already handles the reasoning. The prompt defines how I want the result delivered. JSON escaping matters with LaTeX There is one small detail that can cause confusing bugs. LaTeX uses backslashes. For example: \frac{3}{4} JSON strings also use backslashes for escaping. That means serialized JSON may contain: { "math": "\\[\\frac{3}{4}\\]" } The double backslashes are normal. After JavaScript parses the JSON, the string becomes the LaTeX expression that MathJax needs. This matters for commands such as: \frac \sqrt \times \div \int Incorrect escaping can break otherwise valid AI output. Loading MathJax A page can load MathJax from a content delivery network. A simple setup can look like this: window.MathJax = { tex: { inlineMath: [ ['\\(', '\\)'] ], displayMath: [ ['\\[', '\\]'] ] } }; I prefer explicit delimiters. Inline math uses: \( x = 6 \) Display math uses: \[ x = 6 \] This also avoids depending only on dollar signs. Dollar signs can appear in normal text. Take this question: A $50 product gets a 20% discount. What is the new price? Using $...$ as the only delimiter can create problems when the input contains currency. The \(...\) and \[...\] delimiters make the boundary much clearer. Dynamic AI output needs another MathJax step This part matters in an AI calculator. MathJax can process equations that already exist when the page loads. AI responses arrive later. The user enters a problem. JavaScript sends a request. The server returns the solution. JavaScript then adds those new steps to the page. MathJax needs to process that new content. A simplified renderer could look like this: async function renderSolution(solution) { const container = document.querySelector("#solution"); container.replaceChildren(); solution.steps.forEach((step, index) => { const section = document.createElement("section"); const heading = document.createElement("h3"); heading.textContent = `Step ${index + 1}`; const explanation = document.createElement("p"); explanation.textContent = step.explanation; const math = document.createElement("div"); math.textContent = step.math; section.append( heading, explanation, math ); container.appendChild(section); }); const finalAnswer = document.createElement("div"); finalAnswer.textContent = `Final answer: ${solution.finalAnswer}`; container.appendChild(finalAnswer); await MathJax.typesetPromise([container]); } The important part is: await MathJax.typesetPromise([container]); The AI response already exists inside the container at that point. MathJax scans it and finds the LaTeX delimiters. It then renders the equations. Without that final step, users would see raw strings such as: \[\frac{3}{4} + \frac{1}{2}\] instead of properly formatted math. I keep text and equations separate I prefer this structure: { "explanation": "Subtract 5 from both sides.", "math": "\\[2x = 12\\]" } instead of: { "step": "Subtract 5 from both sides so \\(2x = 12\\)" } Keeping them separate gives the frontend more control. The explanation can use normal paragraph styling. The equation can have more space around it. MathJax only needs to handle the mathematical part. The layout also becomes easier to adapt for phones. I do not ask the AI to generate HTML It might seem easier to ask the model for this: Subtract 5 from both sides. 2x = 12 I avoid that approach. The model should return data. The application should create the markup. I prefer: { "explanation": "Subtract 5 from both sides.", "math": "\\[2x = 12\\]" } Then JavaScript creates the elements. This keeps the application structure predictable. It also gives the model less control over the page. I would avoid doing this with raw model output: container.innerHTML = modelResponse; AI output should count as untrusted input. Creating elements and using textContent gives the application much more control. explanation.textContent = step.explanation; math.textContent = step.math; MathJax can process the mathematical string after JavaScript adds it to the document. The model controls the equation. It does not control the page markup. The browser should not contain the AI API key An AI-powered calculator needs access to an AI service. That usually means an API key. The key should stay on the server. It should not appear inside public browser JavaScript. This is unsafe: const API_KEY = "my-secret-api-key"; Anyone can inspect the page source or network activity. Instead, the browser sends the problem to an endpoint on my own server. A simplified example looks like this: async function solveProblem(problem) { const response = await fetch("/api/solve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ problem }) }); if (!response.ok) { throw new Error("Unable to solve the problem"); } return response.json(); } The server talks to the AI provider. The browser never receives the secret key. The server can also apply rate limits and input limits before spending money on a model request. AI still needs verification AI gives the calculator flexibility. It does not guarantee perfection. A language model can make calculation mistakes. It can miss a negative sign. It can misunderstand ambiguous input. It can also give a clean explanation for an incorrect answer. That means the application should verify whatever it can. Take: 2x + 5 = 17 Suppose the model returns: x = 6 The application can substitute 6 back into the equation. 2(6) + 5 = 17 12 + 5 = 17 17 = 17 That gives another signal that the answer works. Basic arithmetic can use deterministic code for checks. A simple comparison might look like this: function verifyBasicArithmetic(expected, modelAnswer) { return Number(expected) === Number(modelAnswer); } More advanced problems need more advanced verification. The main idea stays simple: Use AI for explanation and flexible reasoning. Verify with deterministic tools where possible. A hybrid system makes more sense AI does not need to calculate everything. Take: 25 × 8 JavaScript can solve that perfectly well. const answer = 25 * 8; Sending every basic multiplication problem to an AI model adds cost and latency without much benefit. A calculator can route different problems to different systems. A simplified version might look like this: function chooseSolver(problem) { if (isBasicArithmetic(problem)) { return "local"; } return "ai"; } Simple arithmetic can use local code. More complex questions can use the AI layer. A larger system could also use dedicated math libraries for symbolic work. Different tools can handle different jobs. There is no good reason to force every problem through one solver. Word problems show where AI helps Natural-language math questions create a good use case for AI. Take this: A train travels 240 kilometers in 3 hours. What is its average speed? A traditional calculator first needs to extract: distance = 240 km time = 3 hours Then it needs to identify the formula: speed = distance ÷ time Then it calculates: 240 ÷ 3 = 80 An AI model can understand the sentence and produce those steps together. A structured response might look like this: { "finalAnswer": "\\(80\\text{ km/h}\\)", "steps": [ { "explanation": "Use the average speed formula.", "math": "\\[\\text{speed} = \\frac{\\text{distance}}{\\text{time}}\\]" }, { "explanation": "Insert the values.", "math": "\\[\\text{speed} = \\frac{240}{3}\\]" }, { "explanation": "Calculate the result.", "math": "\\[\\text{speed} = 80\\text{ km/h}\\]" } ] } MathJax can then turn those LaTeX strings into readable equations. This moves the project beyond a normal calculator. The system now has to understand what the user means before solving the calculation. Users do not always type clean math People enter the same problem in different ways. One person may type: sqrt 144 Another may type: square root of 144 Another may use: √144 All three mean the same thing. Input processing can clean some common formatting before sending the request to a solver. A basic example looks like this: function normalizeInput(input) { return input .trim() .replace(/\s+/g, " "); } Real math normalization needs more care. Changing mathematical input too aggressively can change its meaning. I prefer conservative cleanup and let the solver interpret the actual expression. Invalid problems need a clear error path A solver should not invent an answer when the input does not make sense. Take: 5 + × 9 The application should return a clear error. For example: { "status": "error", "message": "I could not read this expression clearly." } That works better than showing a confident but unreliable result. The same rule applies when the AI response does not match the expected JSON structure. The application should reject malformed output instead of trying to guess what the model meant. Public AI tools also need limits A public calculator needs request limits. Without them, someone could submit a huge block of unrelated text. Empty input should fail before reaching the model. if (!problem.trim()) { return error("Enter a math problem first."); } Long input can also have a limit. if (problem.length > MAX_PROBLEM_LENGTH) { return error("The problem is too long."); } The output can have limits too. A solution with 40 tiny steps often makes the problem harder to follow. The goal is not to generate the longest explanation. The goal is to show enough work for someone to understand the calculation. The interface still matters A correct answer can still feel difficult to use. I wanted the solution to have a clear order: Problem ↓ Final Answer ↓ Step-by-Step Work Each equation needs enough room. The explanation should stay short. Important operations should stand out. Complex fractions and equations should remain readable on smaller screens. The user should also be able to enter another problem without reloading the entire page. The AI model handles the reasoning. MathJax handles the notation. The interface still decides whether the final experience feels easy to follow. A live version I built a working version around these ideas. The Step by Step Math Calculator uses AI to help solve different types of math questions and explain the work behind the answer. The important part for me was not just showing a result. I wanted the calculator to turn the solution into readable steps and properly formatted mathematical expressions. That required much more than sending a prompt to an AI model. The architecture I would use again The main pieces now look like this: ┌───────────────────────────┐ │ User Input │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Input Processing │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Solver Router │ └────────┬───────────┬──────┘ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Local Math │ │ AI Model │ └───────┬──────┘ └───────┬──────┘ │ │ └────────┬───────┘ │ ▼ ┌───────────────────────────┐ │ Verification │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Structured JSON │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ LaTeX Output │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ MathJax │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Step-by-Step Result │ └───────────────────────────┘ This separation also makes the project easier to change. A different AI model can replace the current one. A stronger math engine can handle more deterministic calculations. The frontend does not need to know how every solver works. MathJax only needs valid LaTeX. Each layer can change without rebuilding the whole project. The part I underestimated At first, the obvious challenge looked like solving the math. That turned out to be only one part. The harder product problem involved moving from: input → answer to: input ↓ understand ↓ solve ↓ verify ↓ explain ↓ format ↓ render AI helps with understanding and explaining. Normal code handles security and application logic. Deterministic math can verify some results. LaTeX provides a standard format for mathematical expressions. MathJax makes those expressions readable inside the browser. That combination works much better than asking one tool to handle everything. I do not need an AI model to tell me that: 2 + 2 = 4 The interesting cases look more like this: A shop reduces a $120 item by 15%. Sales tax of 5% applies after the discount. What is the final price? The calculator has to understand the order of operations in the real-world problem. It needs to perform the calculations. It needs to explain each step. It needs to format percentages and equations. It needs to display the result clearly. That is where a step-by-step calculator becomes much more interesting than eval().
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to