Fix MQL5 Array Out of Range Errors Safely in MT5
By HorizonAI Team · 12 min read · Intermediate
MQL5 Array Out of Range Error: How to Fix It Safely
An array out of range error is an MT5 runtime failure. Your EA or indicator compiles, then stops during a Strategy Tester run because it reads rates[1], buffer[2], or another element that isn't available yet.
Short answer: MQL5 throws an array-out-of-range error when code reads or writes an index that doesn't exist. Fix it by checking the count returned from CopyBuffer() or CopyRates(), sizing arrays deliberately, and accessing only indexes below ArraySize(array). For market-series code, also confirm that the bar shift you need exists before using it.
The safe sequence is request data, verify the copied count, verify the array size, then calculate. A copy call completing doesn't mean it delivered every bar a later expression needs.
Start with the runtime message, not a random index change
A compiler error identifies syntax or type problems before the program runs. The MT5 Journal or Strategy Tester log reports a runtime array error after the EA or indicator starts executing. They require different fixes.
A typical runtime message names the source file, line, and invalid access. Go to that expression first. If the line is double priorClose = rates[1].close;, it doesn't mean rates[1] is always wrong. It means that on that tick, rates held fewer than two elements.
| Runtime pattern | Meaning | Direct fix |
|---|---|---|
Error at values[0] | A copy call returned zero or failed | Stop, log the issue, retry on a later tick |
Error at values[1] or values[2] | Too few bars were copied | Require at least 2 or 3 copied values |
| Error inside a loop | The loop reaches the size itself | Use < ArraySize(array), never <= |
| Error after changing series orientation | Index meaning changed | Define shifts 0, 1, and 2, then test them |
Error in OnCalculate() | Lookback exceeds rates_total | Return until enough bars exist |
The named line is the symptom. The cause is normally unchecked data loading, a loop boundary, or a hard-coded shift larger than available history.
The boundary rule: valid indexes begin at zero
An array with three values has indexes 0, 1, and 2. Its size is 3. Index 3 is outside the array.
The unsafe loop is for(int i = 0; i <= ArraySize(values); i++), because the final iteration accesses one position beyond the last element. Write i < ArraySize(values) instead.
ArraySize() should be the source of truth, not a remembered constant. The MQL5 ArraySize reference defines it as the number of elements in the first dimension, including the allocation currently held by a dynamic array.
That distinction matters in trading code. You can resize a dynamic array to three elements while price history or an indicator buffer still has fewer than three usable values. Reserving container space doesn't prove the data is ready.
Treat CopyBuffer() and CopyRates() returns as gates
CopyBuffer() reads values from an indicator buffer. CopyRates() reads bar structures. Both return the number of elements actually copied, or -1 on failure. The returned count is the guard that prevents most array errors.
For a moving-average cross confirmed on closed candles, shifts 1 and 2 are required. Shift 0 is the forming bar, shift 1 is the most recently closed bar, and shift 2 is the candle before it. Requesting three values also gives the EA a timestamp for new-bar detection.
This is unsafe: call CopyBuffer(handle, 0, 0, 3, fast); and immediately evaluate fast[2] <= slow[2] && fast[1] > slow[1]. Index 2 must be proved available before it is read.
Use this sequence every time:
- Reset the previous error before a copy, so the diagnostic belongs to the current request.
- Store each return count.
- Require the count your calculation needs, not merely a non-negative result.
- Check
ArraySize()before indexing. - Print counts and error codes when the guard blocks execution.
The MQL5 CopyBuffer documentation notes that indicator data may not be ready immediately while values are being built. A partial copy is a normal startup state, not permission to index a missing value. CopyRates uses the same count-first approach for price bars.
Build a safe moving-average crossover EA reader
The complete EA below demonstrates a reusable boundary around two EMA buffers and price bars. It isn't presented as a promise of a trading edge. Its job is to show how safe data access should look.
It requests three entries, rejects partial results, sets series direction, and logs why it can't continue. It evaluates completed bars only, uses a 300-point stop loss and 600-point target, and permits one position per symbol.
#property copyright "HorizonAI"
#property version "1.00"
#include <Trade/Trade.mqh>
input int FastEMAPeriod = 9;
input int SlowEMAPeriod = 21;
input double VolumeLots = 0.10;
input int StopLossPoints = 300;
input int TakeProfitPoints = 600;
input ulong MagicNumber = 260101;
CTrade trade;
int fastHandle = INVALID_HANDLE;
int slowHandle = INVALID_HANDLE;
datetime lastProcessedBar = 0;
// Returns true only when shifts 0, 1, and 2 are safe to read.
bool ReadCrossoverData(double &fastValues[], double &slowValues[], MqlRates &priceBars[])
{
const int requiredBars = 3;
ArrayResize(fastValues, requiredBars);
ArrayResize(slowValues, requiredBars);
ArrayResize(priceBars, requiredBars);
// Index 0 is the forming bar, index 1 is the last closed bar.
ArraySetAsSeries(fastValues, true);
ArraySetAsSeries(slowValues, true);
ArraySetAsSeries(priceBars, true);
ResetLastError();
int fastCopied = CopyBuffer(fastHandle, 0, 0, requiredBars, fastValues);
int fastError = GetLastError();
ResetLastError();
int slowCopied = CopyBuffer(slowHandle, 0, 0, requiredBars, slowValues);
int slowError = GetLastError();
ResetLastError();
int ratesCopied = CopyRates(_Symbol, _Period, 0, requiredBars, priceBars);
int ratesError = GetLastError();
if(fastCopied < requiredBars || slowCopied < requiredBars || ratesCopied < requiredBars)
{
PrintFormat("Data not ready: fast=%d err=%d, slow=%d err=%d, rates=%d err=%d",
fastCopied, fastError, slowCopied, slowError, ratesCopied, ratesError);
return false;
}
if(ArraySize(fastValues) < requiredBars || ArraySize(slowValues) < requiredBars || ArraySize(priceBars) < requiredBars)
{
PrintFormat("Unexpected array size: fast=%d, slow=%d, rates=%d",
ArraySize(fastValues), ArraySize(slowValues), ArraySize(priceBars));
return false;
}
return true;
}
int OnInit()
{
fastHandle = iMA(_Symbol, _Period, FastEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
slowHandle = iMA(_Symbol, _Period, SlowEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
{
PrintFormat("Failed to create EMA handle. Error=%d", GetLastError());
return INIT_FAILED;
}
trade.SetExpertMagicNumber(MagicNumber);
trade.SetDeviationInPoints(20);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(fastHandle != INVALID_HANDLE)
IndicatorRelease(fastHandle);
if(slowHandle != INVALID_HANDLE)
IndicatorRelease(slowHandle);
}
void OnTick()
{
double fastValues[];
double slowValues[];
MqlRates priceBars[];
if(!ReadCrossoverData(fastValues, slowValues, priceBars))
return;
if(priceBars[0].time == lastProcessedBar)
return;
lastProcessedBar = priceBars[0].time;
bool bullishCross = fastValues[2] <= slowValues[2] && fastValues[1] > slowValues[1];
bool bearishCross = fastValues[2] >= slowValues[2] && fastValues[1] < slowValues[1];
if(!bullishCross && !bearishCross)
return;
if(PositionSelect(_Symbol))
return;
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
{
PrintFormat("SymbolInfoTick failed. Error=%d", GetLastError());
return;
}
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
if(bullishCross)
{
double stopLoss = NormalizeDouble(tick.ask - StopLossPoints * _Point, digits);
double takeProfit = NormalizeDouble(tick.ask + TakeProfitPoints * _Point, digits);
if(!trade.Buy(VolumeLots, _Symbol, 0.0, stopLoss, takeProfit, "EMA cross buy"))
PrintFormat("Buy request failed. Retcode=%d", trade.ResultRetcode());
}
else if(bearishCross)
{
double stopLoss = NormalizeDouble(tick.bid + StopLossPoints * _Point, digits);
double takeProfit = NormalizeDouble(tick.bid - TakeProfitPoints * _Point, digits);
if(!trade.Sell(VolumeLots, _Symbol, 0.0, stopLoss, takeProfit, "EMA cross sell"))
PrintFormat("Sell request failed. Retcode=%d", trade.ResultRetcode());
}
}
ArraySetAsSeries(..., true) sits next to the copy calls so the rest of the EA can use familiar bar shifts: [0] is now, [1] is the last closed candle, and [2] is the prior closed candle. Set direction explicitly for each array whose indexes you intend to interpret.
A useful diagnostic is to print values only after the guard passes: PrintFormat("fast[2]=%.5f fast[1]=%.5f", fastValues[2], fastValues[1]);. If that message prints, the indexes existed on that tick.
Reproduce the issue safely in Strategy Tester
You don't need live conditions to prove a guard works. Attach the EA in MT5 Strategy Tester to a symbol and timeframe with limited history loaded, then watch the Journal on early test ticks.
First, remove the partial-copy if block and run the code. Depending on local history and indicator calculation timing, the unsafe build can reach the crossover expression before all three values exist. Restore the block and rerun it. The corrected EA logs Data not ready and returns rather than indexing a missing element.
Then test the configuration boundary. Change requiredBars to 2 while leaving fastValues[2] in the cross expression. The size guard catches the configuration error. Set it back to 3, recompile, and confirm the tester log stays clean during initialization.
If trades still don't appear once data access is safe, that is a separate signal or order issue. Use this MT5 EA not opening trades checklist after you have proven the data reads.
ArraySetAsSeries() changes index meaning, not capacity
It's easy to blame ArraySetAsSeries() because an error appears after adding it. The function doesn't create elements. It controls how you address time-oriented data.
With series indexing enabled, 0 means the newest requested bar and larger indexes move backward in history. The practical fix is to state your desired direction, request enough bars, and validate the values against the chart.
There is a subtle implementation detail: the CopyBuffer reference says copied data places the oldest element at the start of physical memory regardless of the as_series property. Keep the series call in your code so index-facing logic is unambiguous, then test output rather than inheriting assumptions from another project.
Guard loops, dynamic arrays, and custom indicators
The same boundary discipline applies beyond EAs.
Dynamic-array loops
When a dynamic array comes from a copy call, drive loops from the returned count or from ArraySize() after a guard. If all requested values are required, the returned count is the stricter check. The safe form is for(int i = 0; i < copied; i++), after copied has been verified. If copied is zero, the loop runs zero times. That's correct.
Indicators and OnCalculate()
For a custom indicator, rates_total is the number of bars the terminal passed to the calculation call. Don't calculate a 50-bar lookback or access close[50] until at least 51 bars are present. The necessary early-return guard is if(rates_total < 51) return 0;.
The OnCalculate documentation exposes rates_total and prev_calculated so an indicator can determine available data and the part that needs recalculation. Apply the same check before indexing indicator buffers and price arrays.
Hard-coded shifts
A shift isn't valid merely because it worked on EURUSD H1. A newly opened chart, nonstandard symbol, higher timeframe, or an indicator's first call may not yet provide the history you expect. Treat every fixed shift as a minimum-bar requirement.
For example, close[20] requires at least 21 accessible elements. A 200-period SMA needs at least 200 bars for its first value, and a crossover calculation around it usually needs another closed bar for comparison.
Use this compact diagnosis checklist
- Find the actual index. Is the code accessing
[0],[1],[2], or a loop variable? - Count what the expression needs. Accessing
[2]requires three elements, not two. - Capture copy returns. Store
CopyBuffer()andCopyRates()results inintvariables and require the needed count. - Check capacity. Use
ArraySize()before access, especially afterArrayResize()or a helper call. - Fix the loop edge. The final valid index is
ArraySize(array) - 1, so use<, not<=. - State the series direction. Call
ArraySetAsSeries()and document each bar shift. - Guard startup. In
OnCalculate(), return untilrates_totalexceeds your largest lookback and shift. - Print context. Log copied counts, array sizes, requested bars, and
GetLastError()before returning.
This checks the cause faster than repeatedly changing the index at the failing line. Replacing [2] with [1] is only valid if the strategy logic truly needs two elements.
Common mistakes that keep the error alive
❌ Mistake: Checking only for -1. Accepting CopyBuffer() results of 0, 1, or 2 and then reading [2] still fails.
✅ Do this: Require the exact minimum: if(copied < 3) return false;. Use a named requiredBars constant so the request, guard, and later indexes remain aligned.
❌ Mistake: Resizing to 100 and assuming 100 valid bars arrived. ArrayResize() reserves array space. It doesn't force the terminal to fill history or calculate indicator values.
✅ Do this: Validate the returned count. If calculation needs 50 values, reject a result below 50 even if ArraySize(values) is 100.
❌ Mistake: Running signals on buffer[0] because it exists. The current bar can change before it closes, mixing a data-safety repair with unstable signal logic.
✅ Do this: Use [1] and [2] to confirm crossover signals on closed candles. Use [0].time only to detect a newly opened bar.
❌ Mistake: Using <= in a copied loop. One extra iteration produces an invalid final index.
✅ Do this: Write i < count, where count is the verified copied amount or ArraySize() after validation.
Pro tips for reusable MQL5 data helpers
Keep the guard beside the request. A helper like ReadCrossoverData() prevents one EA component from validating a buffer while another indexes it later. Return false and skip that tick when data is incomplete.
Log counts, not “failed.” fast=2, slow=3, rates=3 tells you which dependency is short. It is far more actionable than an exception after the fact.
Make lookback requirements explicit. If a feature reads shifts 1 and 2, use const int requiredBars = 3. When you add a 50-bar comparison, update one clear requirement.
Separate data safety from order debugging. Once counts and sizes are correct, inspect position rules, symbol filling settings, stops, and trade retcodes separately. For position identification across multiple EAs, see MQL5 magic numbers.
Generating this guard pattern without writing it yourself
HorizonAI can generate, edit, and compile-check MQL5 EAs from a plain-English specification. For this pattern, specify the safety requirements, not just “make an EMA bot.”
Build an MQL5 Expert Advisor using a 9 EMA and 21 EMA crossover on closed bars. Create a reusable helper that uses CopyBuffer and CopyRates to request shifts 0, 1, and 2. Require all three copied values, check ArraySize before indexing, set arrays as series, and PrintFormat the copied counts and GetLastError values when data is unavailable. Enter only one position per symbol with 0.10 lots, a 300-point stop loss, and a 600-point take profit. Use a magic number of 260101 and explain each array safety guard.
It returns working MQL5 code you can edit in chat, with real compiler validation rather than guessed-at fixes. HorizonAI writes the EA code; you choose how to run it in MetaTrader 5. Try it free →
FAQs
Why does MQL5 compile when the array index is wrong?
The compiler can confirm that an index expression has the correct type, but it often can't know how many values an array will hold on a future tick. The runtime error appears only when the EA or indicator accesses an unavailable element.
Does ArrayResize() stop an array out of range error?
Only if the code then stays inside resized bounds. It doesn't prove that CopyBuffer() or CopyRates() supplied sufficient usable market data for the indexes your logic reads.
How many bars do I need for buffer[2] in MQL5?
You need at least three elements. With series indexing, they are shifts 0, 1, and 2, so guard with copied >= 3 before reading buffer[2].
Should an EMA crossover EA use bar 0 or bar 1?
Use bars 1 and 2 to confirm a crossover on completed candles. Bar 0 is useful for detecting a new candle, but its EMA values can change while the candle is forming.
Final thoughts
An array-out-of-range error is a boundary failure, not a mysterious MT5 quirk. Capture copy counts, set the series direction, and make every accessed shift part of an explicit minimum-bar requirement.
The pro move is one guarded data helper reused across EAs and indicators. It turns a runtime crash into a visible Journal message and a skipped tick, which is where incomplete market data belongs.
Related articles
- MT5 EA Not Opening Trades: MQL5 Debugging Checklist — Debug trade conditions after market-data reads are safe.
- MQL5 Magic Numbers: Safely Manage Your EA Positions — Keep multiple EAs from confusing each other’s positions.
- How to Code a Break-Even Stop Expert Advisor in MQL5 — Add a controlled break-even rule to an MT5 EA.
- How to Code a Supertrend Indicator and an EA in MQL5 — Build an indicator-to-EA workflow in MetaTrader 5.
- How to Convert Pine Script to MQL5: A Manual Translation Guide — Translate bar-based strategy logic into MT5 conventions.
- How to Calculate Lot Size by Risk Percentage in MQL5 — Replace fixed lots with risk-based volume sizing.
- How to Use iCustom in an MQL5 Expert Advisor — Load and read a custom indicator safely from an EA.
- MQL5 CopyBuffer Error 4806: Why It Happens and How to Fix It — Diagnose a related indicator-data availability error.
Questions about MQL5 array out of range errors? Join our Discord to discuss with other traders!
