Infix to Postfix in JavaScript
A complete infix to postfix converter in JavaScript — the same shunting-yard
algorithm this site runs on. It works in Node and in the browser, handles
multi-character operands and right-associative ^, and every
example below was run before publishing.
The complete JavaScript program
Save this as infix-to-postfix.js. It uses no dependencies, exports
its functions for reuse, and runs directly from the command line.
/**
* infix-to-postfix.js — convert an infix expression to postfix (RPN)
* using Dijkstra's shunting-yard algorithm.
*
* Run: node infix-to-postfix.js "A + B * C"
*/
'use strict';
const PRECEDENCE = { '+': 1, '-': 1, '*': 2, '/': 2, '%': 2, '^': 3 };
const RIGHT_ASSOCIATIVE = new Set(['^']);
/** Split the expression into operands, operators and brackets. */
function tokenize(expression) {
const tokens = [];
let i = 0;
while (i < expression.length) {
const ch = expression[i];
if (/\s/.test(ch)) { i++; continue; }
// Operands may be multi-character: 12, 3.5, count, x1
if (/[A-Za-z0-9_.]/.test(ch)) {
let j = i;
while (j < expression.length && /[A-Za-z0-9_.]/.test(expression[j])) j++;
tokens.push({ type: 'operand', value: expression.slice(i, j) });
i = j;
continue;
}
if (ch === '(' || ch === ')') {
tokens.push({ type: ch === '(' ? 'lparen' : 'rparen', value: ch });
i++;
continue;
}
if (ch in PRECEDENCE) {
tokens.push({ type: 'operator', value: ch });
i++;
continue;
}
throw new SyntaxError(`Unexpected character "${ch}" at position ${i}`);
}
return tokens;
}
/** The shunting-yard pass itself. */
function toPostfix(expression) {
const output = [];
const stack = [];
for (const token of tokenize(expression)) {
if (token.type === 'operand') {
output.push(token.value);
} else if (token.type === 'lparen') {
stack.push(token.value);
} else if (token.type === 'rparen') {
while (stack.length && stack[stack.length - 1] !== '(') {
output.push(stack.pop());
}
if (!stack.length) throw new SyntaxError('Unbalanced brackets: missing "("');
stack.pop(); // discard the "("
} else {
const op = token.value;
while (stack.length) {
const top = stack[stack.length - 1];
if (top === '(') break;
const higher = PRECEDENCE[top] > PRECEDENCE[op];
const equalLeft = PRECEDENCE[top] === PRECEDENCE[op] && !RIGHT_ASSOCIATIVE.has(op);
if (!higher && !equalLeft) break;
output.push(stack.pop());
}
stack.push(op);
}
}
while (stack.length) {
const top = stack.pop();
if (top === '(') throw new SyntaxError('Unbalanced brackets: missing ")"');
output.push(top);
}
return output.join(' ');
}
/** Evaluate a postfix string, so the conversion can be checked. */
function evaluatePostfix(postfix) {
const stack = [];
for (const token of postfix.split(/\s+/).filter(Boolean)) {
if (token in PRECEDENCE) {
const b = stack.pop();
const a = stack.pop();
if (a === undefined || b === undefined) throw new SyntaxError('Malformed postfix expression');
switch (token) {
case '+': stack.push(a + b); break;
case '-': stack.push(a - b); break;
case '*': stack.push(a * b); break;
case '/': stack.push(a / b); break;
case '%': stack.push(a % b); break;
case '^': stack.push(Math.pow(a, b)); break;
}
} else {
stack.push(Number(token));
}
}
if (stack.length !== 1) throw new SyntaxError('Malformed postfix expression');
return stack[0];
}
module.exports = { toPostfix, evaluatePostfix, tokenize };
if (require.main === module) {
const input = process.argv.slice(2).join(' ') || 'A + B * C - (D / E)';
console.log(`Infix: ${input}`);
console.log(`Postfix: ${toPostfix(input)}`);
}
Run it
Node 12 or newer, no install step:
node infix-to-postfix.js "A + B * C - (D / E)"
Sample output
Infix: A + B * C - (D / E)
Postfix: A B C * + D E / -
Run it with no arguments and it converts that same default expression, which makes it easy to check the file works before you trust it with your own input.
How it works
The algorithm keeps two collections: an output list that only ever grows, and a stack that holds operators and left brackets while they wait for their operands. Every token falls into one of four cases.
- Operand — goes straight to the output. It never touches the stack.
- Left bracket — pushed, where it acts as a fence nothing pops past.
- Right bracket — pop to the output until the matching left bracket surfaces, then discard both brackets. Brackets never appear in postfix.
- Operator — pop while the operator on top binds at least as tightly, then push. That single condition is where precedence and associativity live.
Tokenizing separately from converting is what lets operands be more than one
character. A loop that walks the string character by character would read
x1 as two operands and quietly produce the wrong answer.
The browser version
The conversion logic has no Node dependency. Drop the last two blocks
(module.exports and the require.main check) and the
same file runs in a page:
<script src="infix-to-postfix.js"></script>
<script>
const input = document.querySelector('#expr');
const out = document.querySelector('#out');
input.addEventListener('input', () => {
try {
out.textContent = toPostfix(input.value);
} catch (err) {
out.textContent = err.message; // unbalanced brackets, bad character
}
});
</script>
For an ES module, replace module.exports with
export { toPostfix, evaluatePostfix } and import it as usual.
The two rules that decide correctness
Almost every wrong implementation gets one of these two wrong, and both live in
the same while condition.
Right-associativity of ^
A ^ B ^ C means A ^ (B ^ C), so the correct postfix is
A B C ^ ^ — not A B ^ C ^. That is why the loop pops on
equal precedence only when the incoming operator is not
right-associative:
const equalLeft = PRECEDENCE[top] === PRECEDENCE[op] && !RIGHT_ASSOCIATIVE.has(op);
Where % belongs
Modulo has the same precedence as * and /, not
^. Putting it on the wrong level changes the answer for any
expression that mixes it with multiplication.
Evaluating the postfix result
Evaluation is the easier half: push operands, and on an operator pop two values,
apply, push the result. The order matters — the second value popped is
the left operand, which is what makes - and / come out
the right way round.
const b = stack.pop(); // right operand
const a = stack.pop(); // left operand
stack.push(a - b); // not b - a
Running evaluatePostfix(toPostfix('3 + 4 * 2 / (1 - 5) ^ 2')) returns
3.5, which is a quick way to check a conversion you are unsure about.
Checking your output against this site
If you are implementing this yourself, the converter on this site is available as a JSON endpoint so you can diff your output against it in a test:
const res = await fetch(
'https://infixtopostfixconverter.com/api/convert/?expr=' +
encodeURIComponent('A + B * C')
);
const data = await res.json();
console.log(data.postfix); // "A B C * +"
console.log(data.prefix); // "+ A * B C"
console.log(data.value); // null for letters, a number for numeric input
console.log(data.steps); // the full shunting-yard trace
It runs the same converter.js the page you are reading loads, so the
endpoint and the tool cannot disagree. Invalid input returns HTTP 400 with
error, hint and position fields. Keep the
trailing slash to avoid a redirect hop, and note the response is cached for five
minutes at the edge.
Errors you are likely to hit
| Message | Cause |
|---|---|
Unbalanced brackets: missing ")" |
A ( was still on the stack at the end of input. |
Unbalanced brackets: missing "(" |
A ) arrived with no matching ( below it. |
Unexpected character "$" |
A character that is neither an operand, an operator nor a bracket. |
Malformed postfix expression |
The evaluator ran out of operands, or finished with more than one left. |
NaN from the evaluator |
Letters were evaluated as numbers. Evaluation needs numeric operands. |
FAQ
Does this handle multi-digit numbers and variable names?
Yes. The tokenizer consumes a whole run of letters, digits, underscores and
dots as one operand, so 12.5, count and
x1 all convert correctly. This is the single most common bug in
character-by-character implementations.
Why a Set for right-associativity instead of a list of operators?
Membership is the only question ever asked of it, and a
Set answers that in constant time while reading as what it is.
With one entry the performance is irrelevant; the clarity is the point.
Can I use this for prefix as well?
Yes, with two extra steps: reverse the input and swap every bracket, run
toPostfix on that, then reverse the result. The
infix to prefix converter shows the
same derivation running live.
Is unary minus supported?
Not in this listing — -A + B would treat - as a
binary operator with nothing to its left. Handling it properly means
detecting an operator in operand position and emitting a distinct unary
token, which is what the converter on this site does.
Check your output: paste any expression into the converter to see the same result with the full stack trace, or compare implementations on the Python, Java, C++, C, Go, Rust and C# pages.