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
/**
 * 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:

In a browser
<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:

GET /api/convert/
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

MessageCause
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.