You add a line that reads the third most recent pivot, run the script, and the chart greets you with a red runtime error: "Index 2 is out of bounds, array size is 2." The script compiled fine. It even worked for the first several hundred bars. Then one bar arrived where the array had fewer elements than your code assumed, and the whole indicator stopped.
Arrays are the most used collection in Pine Script, and the most common source of that kind of error. They are simple in concept: an ordered list of values of one type. The trouble is that Pine runs your code once per bar, so an array is never a static thing. It grows, it gets trimmed, it resets, and every read has to survive the moments when it is still short. This guide covers how to create arrays, store values in them, loop over them, sort them, and guard every read so the out-of-bounds error never reaches your chart.
If you want the wider picture of matrices and maps, the arrays, matrices, and maps overview covers when to use each. Here we stay with arrays and go deeper.
Declaring an array that survives from bar to bar
An array is a reference type, and the first decision is where it lives. If you declare it with a plain assignment, Pine creates a new one on every bar, so anything you stored on the previous bar is gone. To keep the contents across bars, declare it with var, which initializes it once on the first bar and then keeps the same object for the rest of the run. The var and varip guide explains why in detail, and the execution model post shows where the per-bar rerun comes from.
You create an array with a typed constructor. In v6 the preferred form is the generic array.new<type>(), though the older array.new_float() and array.new_int() names still work. You can set an initial size and an initial value, and you can also build one from literal values with array.from().
//@version=6
indicator("Array declarations", overlay = true)
// Empty arrays that persist across bars
var float[] closes = array.new<float>()
var int[] barIndexes = array.new_int()
// Pre-sized: 5 slots, all starting at 0.0
var float[] slots = array.new<float>(5, 0.0)
// Built from literals
var string[] names = array.from("fast", "slow", "signal")
if barstate.isfirst
array.push(closes, close)
array.push(barIndexes, bar_index)
plot(array.size(slots), "slot count")An array holds exactly one type. A float[] cannot also hold a string, and a float[] stores na in the same slot that would hold a real number, which matters later when you compute statistics over it. Arrays also have a size limit of 100,000 elements, which a script that appends on every bar of a long chart can reach without anyone noticing.
Adding, reading, and removing values
Four functions cover most of the work. array.push adds to the end. array.get reads by index, starting at zero. array.set replaces the value at an index. array.size tells you how many elements exist right now. The first element is at index 0, so the last valid index is always array.size(a) - 1, and that off-by-one is behind a large share of out-of-bounds errors.
To take things out, array.pop removes and returns the last element, array.shift removes and returns the first, and array.remove deletes by index. To put things in the middle, array.insert places a value at a given index and shifts everything after it. array.unshift adds to the front.
The most common real pattern is a rolling window: push the newest value, and when the array grows past a fixed length, shift the oldest one off the front.
//@version=6
indicator("Rolling window", overlay = true)
int windowLen = input.int(20, "Window length", minval = 2)
var float[] window = array.new<float>()
array.push(window, close)
if array.size(window) > windowLen
array.shift(window)
// Only compute once the window is full
float windowAvg = array.size(window) == windowLen ? array.avg(window) : na
float windowHigh = array.size(window) == windowLen ? array.max(window) : na
plot(windowAvg, "window average", color = color.orange)
plot(windowHigh, "window high", color = color.teal)Notice the check for a full window before calling the reducers. Built-in functions such as array.avg, array.max, array.min, array.sum, array.median, and array.stdev all work on whatever is in the array, so on the first few bars they describe a window that is not yet the size you intended. Gating on the size keeps the plot honest.
Looping over an array
You can loop with an index or with a value. The index form is for i = 0 to array.size(a) - 1, and it is the one to use when you need the position of each element or need to modify the array as you go. The value form is for value in a, which is shorter and cannot go out of bounds because it never touches an index. If you need both, for [i, value] in a gives you each pair.
//@version=6
indicator("Looping an array", overlay = true)
var float[] recentHighs = array.new<float>()
if not na(ta.pivothigh(high, 5, 5))
array.push(recentHighs, high[5])
if array.size(recentHighs) > 8
array.shift(recentHighs)
// Value form: no index, no bounds risk
int above = 0
for level in recentHighs
if close > level
above += 1
// Index-and-value form
float nearest = na
float nearestGap = na
for [i, level] in recentHighs
float gap = math.abs(close - level)
if na(nearestGap) or gap < nearestGap
nearestGap := gap
nearest := level
plot(nearest, "nearest high", style = plot.style_circles)
plotchar(above, "levels below close", "", location.top)One habit saves a lot of grief: do not remove elements from an array while looping forward over it by index. Removing shifts every later element down by one, so the loop skips an element and eventually walks past the end. If you must delete while iterating, loop backward from the last index to zero, or build a second array of the values you want to keep.
Loops also cost time. Every loop runs on every bar, so a loop over a 100 element array on a 5,000 bar chart is half a million iterations. Where a built-in reducer does the job, prefer it, and the slow script guide covers how to find which loop is costing you.
Sorting and searching
array.sort sorts in place, ascending by default. Pass order.descending to flip it. Because it changes the array you pass in, sort a copy made with array.copy if you still need the original order, for example when the array is a chronological record.
When you need to know which element came where, rather than just the sorted values, array.sort_indices returns an array of indexes that would sort the original. That lets you rank one array by another: sort the indexes by price, then use them to read the matching labels.
//@version=6
indicator("Sort and rank", overlay = true)
var float[] levels = array.new<float>()
if not na(ta.pivotlow(low, 4, 4))
array.push(levels, low[4])
if array.size(levels) > 6
array.shift(levels)
// Sort a copy so the chronological order is preserved
float[] ranked = array.copy(levels)
array.sort(ranked, order.descending)
float strongest = array.size(ranked) > 0 ? array.get(ranked, 0) : na
float second = array.size(ranked) > 1 ? array.get(ranked, 1) : na
plot(strongest, "highest stored low", color = color.green)
plot(second, "second highest", color = color.lime)For searching, array.includes returns a boolean for whether a value is present, array.indexof returns the first position of a value or -1 if it is missing, and array.binary_search is faster on a sorted array but only gives correct answers when the array really is sorted. Floating point values rarely match exactly, so searching a float[] for an equal value is usually a mistake. Search integers, strings, or bar indexes, and compare floats with a tolerance instead.
To pull out part of an array, array.slice returns a view of a range. It is a window onto the original, not an independent copy, so changing the slice changes the source. Wrap it in array.copy if you want a separate array.
The out-of-bounds error and how to prevent it
Reading or writing an index that does not exist stops the script with a runtime error. Pine does not return na for a bad index, the way some languages return null. The error tells you the index and the size, which is useful once you know where to look, and it often appears only on certain bars, so a script can run cleanly for a long stretch before it fails.
There are three usual causes. The first is reading before the array is filled, such as calling array.get(a, 2) when fewer than three values have been pushed. The second is the off-by-one: looping to array.size(a) instead of to array.size(a) - 1. The third is an array that was emptied by a removal and then read as though it still held its last value.
The fix in all three cases is to check the size before you read, and to wrap the check in a small function so you write it once.
//@version=6
indicator("Safe reads", overlay = true)
// Returns na instead of throwing when the index is out of range
safeGet(float[] a, int idx) =>
idx >= 0 and idx < array.size(a) ? array.get(a, idx) : na
var float[] swings = array.new<float>()
if not na(ta.pivothigh(high, 3, 3))
array.push(swings, high[3])
// Latest swing high, and the one before it, with no crash on early bars
float latest = safeGet(swings, array.size(swings) - 1)
float previous = safeGet(swings, array.size(swings) - 2)
plot(latest, "latest swing", color = color.red)
plot(previous, "previous swing", color = color.maroon)Because array.size(swings) - 1 is -1 on an empty array, the guard checks idx >= 0 as well as the upper bound. Newer versions of Pine also provide array.first and array.last, which read the ends of an array, but they still raise an error on an empty one, so the size check is not optional.
The function version of this guard has a second benefit. Every read that goes through it returns na when the data is not there, and na flows through the rest of your math in a predictable way. The na handling guide explains how to keep that propagation from hiding bugs, and it is the natural companion to this pattern.
Arrays of drawings and the object limit
Arrays are the standard way to keep track of lines, boxes, and labels you have drawn so you can delete the old ones. Pine caps the number of drawing objects per script, 500 for each kind, and once you pass it the oldest are dropped on their own. Holding them in an array lets you manage the cleanup deliberately instead of losing the wrong ones.
//@version=6
indicator("Track recent labels", overlay = true)
var label[] marks = array.new<label>()
int maxMarks = 20
if ta.crossover(close, ta.sma(close, 50))
array.push(marks, label.new(bar_index, low, "cross", style = label.style_label_up))
if array.size(marks) > maxMarks
label.delete(array.shift(marks))The array.shift call removes the oldest label from the array and returns it, and label.delete removes it from the chart. The result is a fixed-size trail of recent markers. The drawing objects guide and the label and line limits post go further on this pattern.
Common mistakes worth checking before you blame the array
A few habits cause most of the surprises. Declaring an array without var and then wondering why it is always empty is the first. Pushing a value on every bar and never trimming, which grows the array until it hits the size limit, is the second. Comparing floats for equality when searching is the third. Mutating an array passed into a function and forgetting that the caller sees the change is the fourth, since arrays are passed by reference. If a function should not alter its input, copy it first.
It also helps to remember that arrays are for data you need to look at as a group. If you only ever need the last value, a plain variable with var and the history operator is simpler and faster. Reach for an array when the group matters: a window, a set of levels, a stack of recent signals.
Where this leaves you
You now have the working set: declare with var, push and shift to keep a bounded window, loop by value when you can, sort a copy, and read through a size guard. Those five habits remove most array problems before they appear.
When an array script does fail at runtime, the usual cost is the copy-paste loop between TradingView and whatever you used to write the code. PineScripter's AI editor checks every script for compile errors the way TradingView does, and when it finds one the AI sees the error and fixes it, then edits only the affected lines on follow-up requests. Runtime index errors still depend on your data, so the Chart lets you run the script on real market data first and step through it with bar replay to see which bar the array was short on. The runtime errors guide lists the other failures worth knowing.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.
Frequently asked questions
How do I get the last element of a Pine Script array?
Use array.last(a) or array.get(a, array.size(a) - 1). Both raise a runtime error on an empty array, so check array.size(a) > 0 first.
Why is my Pine Script array always empty?
Most often it was declared without var, so a new empty array is created on every bar. Declare it with var to keep it across bars.
Does array.sort change the original array?
Yes, it sorts in place. Make a copy with array.copy and sort the copy if you need to keep the original order.
What causes the out of bounds error in Pine Script arrays?
Reading or writing an index that does not exist, such as array.get(a, 2) on an array with two elements, or looping to array.size(a) instead of array.size(a) - 1.
What is the maximum size of a Pine Script array?
An array can hold up to 100,000 elements. Trim it as you go if your script appends a value on every bar.
Describe your indicator in plain English and get Pine Script that compiles →
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.