insertBefore(data, index) {
    
        /*
         * Create a new list node object and store the data in it.
         * This node will be inserted into the existing list.
         */
        const newNode = new CircularLinkedListNode(data);
                
        // special case: no nodes in the list yet
        if (this[tail] === null) {
            throw new RangeError(`Index ${index} does not exist in the list.`);
        }

        /*
         * Special case: if `index` is `0`, then no traversal is needed
         * and we need to update `this[tail]` to point to `newNode`. First,
         * set `node.next` to the current `this[tail]` so the previous
         * head of the list is now the second node in the list. Then it's
         * safe to update `this[tail]` to point to `newNode`.
         */
        if (index === 0) {
            newNode.next = this[tail].next;
            this[tail].next = newNode;
        } else {

            /*
             * The `current` variable is used to track the node that is being
             * used inside of the loop below. It starts out pointing to
             * `this[tail].next` and is overwritten inside of the loop.
             *
             * The `previous` variable tracks one step behind `current`, which
             * is necessary because we need to adjust the node at `index`-1's
             * `next` pointer to point to the new node.
             */
            let current = this[tail].next,
                previous = null;

            /*
             * The `i` variable is used to track how deep into the list we've
             * gone. This important because it's the only way to know when
             * we've hit the `index` to insert into.
             */
            let i = 0;

            /*
             * Traverse and make sure to keep track of how many nodes have
             * been visited and update the `previous` pointer in addition to
             * `current`. When `i` is the same as `index`, it means we've
             * found the location to insert the new data.
             */
            while ((current.next !== this[tail].next) && (i < index)) {
                previous = current;
                current = current.next;
                i++;
            }
    
            /*
             * At this point, `current` is either the item to insert the new data
             * before, or the last item in the list. The only way to tell is if
             * `i` is still less than `index`, that means the index is out of range
             * and an error should be thrown.
             */
            if (i < index) {
                throw new RangeError(`Index ${index} does not exist in the list.`);
            }

            /*
             * If code continues to execute here, it means `current` is the node
             * to insert new data before and `previous` is the node to insert
             * new data after. So `previous.next` must point to `newNode` and
             * `node.next` must point to `current`.
             */
            previous.next = newNode;
            newNode.next = current;
        }
    }
Inserts some data into the middle of the list. This method traverses
the existing list and places the data in a new node at a specific index.
@param {*} data The data to add to the list.
@param {int} index The zero-based index at which to insert the data.
@returns {void}
@throws {RangeError} If the index doesn't exist in the list.