Queues and Stacks in JavaScript: Core Data Structures

Learn how queues and stacks work in JavaScript with simple explanations and practical examples. Understand LIFO and FIFO concepts, their implementations, and real-world use cases.

Published on 16 May 2026
Reading Time 5
Number of Words 1022

Queues and Stacks in JavaScript: Core Data Structures

Queues and stacks are fundamental abstract data structures. They power many algorithms and help manage collections of data in controlled ways. In JavaScript, you can implement them using arrays (or custom linked structures). Below is a clear guide to both, how they differ, when to use them, and sample code.


Table of Contents

  1. What Are Stacks & Queues?
  2. Stack: LIFO (Last In, First Out)
  3. Queue: FIFO (First In, First Out)
  4. Implementations in JavaScript
    • Using Arrays
    • Using Linked Lists
  5. Use Cases & Examples
  6. Performance Considerations
  7. Tips & Best Practices


1. What Are Stacks & Queues?

  • Stack: A collection where the last element added (pushed) is the first one removed (popped). Think of a stack of plates.
  • Queue: A collection where the first element added is also the first one removed. Think of a line (queue) of people.

The difference is in the order of insertion and removal:

  • Stacklast in, first out
  • Queuefirst in, first out


2. Stack: LIFO (Last In, First Out)

Core operations

  • push(item): add an item to the top
  • pop(): remove and return the top item
  • peek() (or top): view the top item without removing
  • isEmpty(): check whether the stack is empty
  • size(): number of items in the stack

Example: Stack using array

class Stack {
  constructor() {
    this.items = [];
  }

  push(element) {
    this.items.push(element);
  }

  pop() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items.pop();
  }

  peek() {
    return this.items[this.items.length - 1];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  size() {
    return this.items.length;
  }
}

// Demo
const stack = new Stack();
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack.peek());   // 30
console.log(stack.pop());    // 30
console.log(stack.pop());    // 20
console.log(stack.isEmpty()); // false
console.log(stack.pop());    // 10
console.log(stack.isEmpty()); // true


3. Queue: FIFO (First In, First Out)

Core operations

  • enqueue(item): add an item at the back
  • dequeue(): remove and return the front item
  • front() or peek(): view the front item without removal
  • isEmpty()
  • size()

Example: Queue using array

class Queue {
  constructor() {
    this.items = [];
  }

  enqueue(element) {
    this.items.push(element);
  }

  dequeue() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items.shift();
  }

  front() {
    return this.items[0];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  size() {
    return this.items.length;
  }
}

// Demo
const queue = new Queue();
queue.enqueue('Alice');
queue.enqueue('Bob');
queue.enqueue('Charlie');
console.log(queue.front());   // "Alice"
console.log(queue.dequeue()); // "Alice"
console.log(queue.front());   // "Bob"
console.log(queue.isEmpty()); // false
console.log(queue.dequeue()); // "Bob"
console.log(queue.dequeue()); // "Charlie"
console.log(queue.isEmpty()); // true


4. Alternative Implementation: Linked List

Using an array is simple, but operations like shift() (removing from front) are O(n) because elements must be reindexed. If you expect many dequeue operations, a linked list–based queue is more efficient (O(1) enqueue & dequeue).

Queue via singly linked list

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedQueue {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  enqueue(value) {
    const newNode = new Node(value);
    if (!this.tail) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
  }

  dequeue() {
    if (!this.head) {
      return undefined;
    }
    const removedValue = this.head.value;
    this.head = this.head.next;
    if (!this.head) {
      this.tail = null;
    }
    this.length--;
    return removedValue;
  }

  front() {
    return this.head ? this.head.value : undefined;
  }

  isEmpty() {
    return this.length === 0;
  }

  size() {
    return this.length;
  }
}

// Demo
const lq = new LinkedQueue();
lq.enqueue(1);
lq.enqueue(2);
lq.enqueue(3);
console.log(lq.front());     // 1
console.log(lq.dequeue());   // 1
console.log(lq.dequeue());   // 2
console.log(lq.dequeue());   // 3
console.log(lq.isEmpty());   // true

You can likewise build a linked stack if needed, using push/pop on the head of the list.


5. Use Cases & Examples

Understanding when to use stacks vs queues is key.

Use-cases for Stacks

  • Backtracking / undo functionality
  • Function call stack / recursion
  • Syntax parsing / parentheses matching

Example: checking for balanced parentheses:

function isBalanced(str) {
  const stack = new Stack();
  const pairs = { ')': '(', ']': '[', '}': '{' };

  for (const ch of str) {
    if (['(', '[', '{'].includes(ch)) {
      stack.push(ch);
    } else if ([')', ']', '}'].includes(ch)) {
      if (stack.isEmpty() || stack.pop() !== pairs[ch]) {
        return false;
      }
    }
  }

  return stack.isEmpty();
}

console.log(isBalanced("({[]})")); // true
console.log(isBalanced("(]"));     // false

Use-cases for Queues

  • Job scheduling / task queues
  • Breadth-first search (BFS) in graphs/trees
  • Asynchronous message processing / event loops

Example: BFS traversal of a tree:

function bfs(root) {
  const result = [];
  const queue = new Queue();
  queue.enqueue(root);

  while (!queue.isEmpty()) {
    const node = queue.dequeue();
    result.push(node.value);

    if (node.left) queue.enqueue(node.left);
    if (node.right) queue.enqueue(node.right);
  }

  return result;
}

// Suppose a simple binary tree node structure
// Use bfs(rootNode) to get level order traversal


6. Performance Considerations

  • Using Array.shift() for dequeue is O(n) (elements must shift). For many operations, this becomes inefficient.
  • Using a linked list or a circular buffer helps maintain O(1) enqueue & dequeue.
  • Be cautious with memory: track references to avoid leaks.
  • For small datasets or light workloads, using arrays may be acceptable and simpler.


7. Tips & Best Practices

  • Pick the right implementation for your expected usage pattern.
  • Keep your API consistent (names like enqueue, dequeue, push, pop, peek).
  • Handle edge cases: empty structures, single elements.
  • Avoid exposing internal state (e.g. internal array). Use class methods instead.
  • Write unit tests for corner conditions (dequeue from empty, etc.).