Process strings of operations
const stack = new Array()


Use the literal notation ([]) instead. Here's an answer on Stack Overflow that elaborates why you should do that instead of new Array().

const duplicate = () =&gt; {
    try {
        const dup = pop();
        push(dup);
        push(dup);            
    } catch(e) {
        throw new Error(e);    
    }
};


The rule of thumb when it comes to using exceptions is to never use them where you can use an if. That is, don't use exceptions as a flow control mechanism. You should only use it if you want to bail out and do no further.

Also, e is already an instance of Error. You only need to do throw e, not throw new Error(e).

Now if you look closely, all the operations are distinct to each other. They also only operate on the stack only. You can easily implement them as pure functions, taking in the current stack, and returning a new representation of the stack.

For example, duplicate can be implemented as a function that takes an array, and returns a new array, with the last item repeated once more:

const duplicate = stack =&gt;
  stack.length &lt; 1 ? -1 
  : [...stack, stack[stack.length - 1]]


add can be implemented as a function that takes an array, and return an new array which replaces the last two items with their sum:

const add = stack =&gt;
  stack.length &lt; 2 ? -1 
  : [...stack.slice(0, -2), stack[stack.length - 1] + stack[stack.length - 2]]


Doing it this way makes it easier to implement each operation without having them becoming dependent on some common state object, or each other.

Here's how I would implement it:



// Implement each operation as distinct, pure functions.
const push = (stack, v) =&gt; [...stack, v]

const pop = stack =&gt; 
  stack.length &lt; 1 ? -1
  : stack.slice(0, -1)

const duplicate = stack =&gt;
  stack.length &lt; 1 ? -1 
  : [...stack, stack[stack.length - 1]]

const add = stack =&gt;
  stack.length &lt; 2 ? -1 
  : [...stack.slice(0, -2), stack[stack.length - 1] + stack[stack.length - 2]]

const subtract = stack =&gt;
  stack.length &lt; 2 ? -1
  : [...stack.slice(0, -2), stack[stack.length - 1] - stack[stack.length - 2]]

// Map operation tokens to their implementation.
// Should we add more operations, we simply add the implementation and reference
// its token in this map
const operations = {
  'DUP': duplicate,
  'POP': pop,
  '+': add,
  '-': subtract
}

const solution = s =&gt; {

  // For each token, parse and process.
  // You can implement this piece using iteration functions, loops, or even recursion.
  // I'm just more accustomed to reduce because it can carry a value across iterations
  // which, in this case, holds the stack for me.
  const result = s.split(' ').reduce((stack, token) =&gt; {
  
    // If any operation returned -1, we return -1 all the way.
    return stack === -1 ? -1
      // If token is a number, push it to the stack.
      : !isNaN(parseFloat(token)) &amp;&amp; isFinite(token) ? push(stack, Number(token))
      // If token is an operation, apply operation.
      : operations[token.toUpperCase()] ? operations[token.toUpperCase()](stack)
      // Unknown token.
      : -1

  // Our stack as initial reduce value, an empty array.
  }, [])

  // If the result is -1, then so be it. Otherwise, return the top of the stack.
  return result === -1 ? -1 : result[result.length - 1]
}

console.log(solution("3 DUP 5 - -"));
console.log(solution("13 DUP 4 POP 5 DUP + DUP + -"));
console.log(solution("5 6 + -"));