jsguides

Array.prototype.at()

at(index)

The at() method returns the element at the specified index in an array. What makes it special is that it accepts negative indices to count from the end of the array - something traditional bracket notation [] cannot do.

Syntax

array.at(index)
  • index: The index of the element to return. Can be positive (from start) or negative (from end).
  • Returns: The element at the specified index, or undefined if the index is out of bounds.

For TypeScript users, the generic signature below shows that at() returns the element type T or undefined when the index is out of bounds, which is useful for type narrowing in conditional branches.

TypeScript Signature

interface Array<T> {
  at(index: number): T | undefined;
}

The examples below demonstrate at() on a small fruit array, covering positive indexing, negative indexing, and out-of-bounds access. Notice how negative values count back from the last element: -1 is always the final item regardless of array length, and the return type for invalid indices is undefined.

Basic Examples

const fruits = ["apple", "banana", "cherry", "date"];

// Positive index - same as bracket notation
console.log(fruits.at(0));    // "apple"
console.log(fruits.at(2));    // "cherry"

// Negative index - count from end
console.log(fruits.at(-1));   // "date" (last element)
console.log(fruits.at(-2));   // "cherry"
console.log(fruits.at(-4));   // "apple"

// Out of bounds returns undefined
console.log(fruits.at(10));   // undefined
console.log(fruits.at(-10));  // undefined

The basic examples confirm that at() behaves like bracket notation for positive indices and adds negative indexing for free. The comparison below shows how at(-1) replaces the older arr[arr.length - 1] pattern with a single, readable expression. The old approach works, but it repeats the array variable and forces the reader to parse arithmetic for what should be a trivial operation.

Why use at() instead of bracket notation?

Before ES2022, getting the last element required arr[arr.length - 1]. With at(), simply use -1:

const items = [1, 2, 3];

// Old way
console.log(items[items.length - 1]);  // 3

// New way with at()
console.log(items.at(-1));              // 3

This is especially useful when:

  • The array length is unknown or dynamic
  • Chaining methods without storing array length
  • Writing readable code for “last element” access
  • Getting second-to-last, third-to-last elements easily

The following examples put at() into practical contexts, from a simple helper that returns the last element to chaining with filter() and map(). Each example shows how at() keeps the code flat and direct instead of burying intent inside arithmetic.

Practical Examples

Getting the last element

function getLastItem(arr) {
  return arr.at(-1);
}

console.log(getLastItem([1, 2, 3]));        // 3
console.log(getLastItem(["a", "b", "c"]));  // "c"
console.log(getLastItem([]));               // undefined

The helper above works on any array without needing a length check. Using -2 instead of -1 gives you the second-to-last element, which is useful for comparing the most recent value against its predecessor in log processing or score tracking.

Getting second-to-last element

const scores = [95, 87, 92, 88, 79];
console.log(scores.at(-2));  // 88 (second highest)

Negative indexing with at() is especially handy when you need a fixed offset from the end, like the penultimate entry in a sorted list. The next example shows at() chained onto filter() and map() results, where the array length is not known ahead of time.

Chaining with other array methods

const data = [10, 20, 30, 40, 50];

// Get last element after filtering
const result = data.filter(x => x > 25).at(-1);
console.log(result);  // 50

// Get last element after mapping
const lastDoubled = data.map(x => x * 2).at(-1);
console.log(lastDoubled);  // 100

Chaining at() onto transformed arrays avoids creating an intermediate variable just to grab the last element. The examples below confirm how at() behaves in edge cases: empty arrays return undefined, sparse arrays expose holes, and indices beyond the array bounds are handled without errors.

Edge Cases

const arr = ["first"];

// Empty result from filter
console.log([].at(-1));  // undefined

// Works with sparse arrays
const sparse = [1, , 3];
console.log(sparse.at(1));  // undefined (not 3)

// Negative index larger than length
console.log(arr.at(-5));  // undefined

Why at() feels more expressive

The main benefit of at() is not just shorter code. It communicates intent. When you see arr.at(-1), the reader immediately knows you want the last item, even if the array length is changing or unknown. With bracket notation, the same idea requires a more mechanical arr[arr.length - 1] expression, which is correct but harder to scan.

This matters in code that works with recent history, queue tails, or user-generated lists. If the collection can be empty, at() also makes the undefined result feel natural, because the method already encodes boundary checking for you instead of forcing you to add it manually.

Choosing between at() and brackets

Use bracket notation when you already have a direct positive index and you do not need any extra behavior. Use at() when you want negative indexing, or when readability is more important than shaving off a few characters. Both approaches are fine, but at() is often easier to teach because it uses one rule for both ends of the array.

One thing to remember is that at() never mutates the array. It is a read-only lookup method, so it is safe to use in expressions, filters, and chained operations. That makes it a good fit for data processing code where you want a concise lookup without changing the original collection.

Browser Compatibility

BrowserVersion
Chrome92+
Firefox90+
Safari15.4+
Edge92+

Polyfill

Legacy environments can use this polyfill:

if (!Array.prototype.at) {
  Array.prototype.at = function(n) {
    n = Math.trunc(n) || 0;
    if (n < 0) n += this.length;
    return n >= 0 && n < this.length ? this[n] : undefined;
  };
}

This polyfill handles negative indices and boundary checks just like the native implementation, ensuring consistent behavior across older JavaScript environments.

See Also