A plain-English walkthrough of the most common Pine Script errors beginners hit on TradingView — mutable-variable scope, na handling, type mismatches, "Cannot call with arguments", and series-vs-simple — with the cause and a tiny correct v6 fix for each. This is educational material about writing correct code, not trading or financial advice; trading involves real risk of loss, and a script that compiles cleanly tells you nothing about whether a strategy is sound.
Pine Script's compiler is strict in ways that surprise people coming from Python or JavaScript. Most of the confusing messages trace back to a single idea: Pine is built around the series — a value that exists once per bar across the whole chart history — and the compiler is constantly checking that the type and scope of your values line up with that model. When the message says something about a "mutable variable" or "series vs simple," it's really telling you that a value isn't the shape the function expected at that point in the bar-by-bar evaluation.
The good news is that the same handful of pine script errors account for the large majority of what beginners hit. Once you can recognise the category from the wording, the fix is usually mechanical. The error text almost always names a line and a column — start there, and read the message literally rather than guessing. "Cannot call 'ta.sma' with argument 'length'=..." is not vague once you know it's a type complaint about the second argument.
Throughout this guide the snippets target Pine v6. If you're copying older forum code, the first thing to check is the version annotation: many "undeclared identifier" surprises are just v5-or-earlier syntax (study(), bare sma(), security()) running under a v6 header. We'll cover that explicitly below. As always, this is about making code compile and behave predictably — none of it implies a script will be profitable, and nothing here is financial advice. Trading involves a real risk of loss.
This is probably the most-Googled Pine error, and it shows up in two distinct flavours. The first is trying to reassign a variable you declared with = (which creates a new immutable binding each bar) instead of := (which reassigns). The fix is to declare once with var (or plain =) and reassign with :=.
The second flavour is the literal message "Cannot use a mutable variable as an argument of...", which appears when you pass a variable you've been reassigning into a function that demands a simple or const argument — classic examples are the length of a moving average or the timeframe of request.security. Those functions want a value fixed for the whole chart, not one you mutate bar to bar.
Here's the reassignment pattern done correctly. Note var for state that persists across bars, and := to update it:
//@version=6
indicator("Mutable variable, done right", overlay = true)
// declare ONCE, persists across bars
var float highestClose = na
// reassign with := (not =)
if barstate.isfirst
highestClose := close
else
highestClose := math.max(nz(highestClose, close), close)
plot(highestClose)If instead the complaint is about passing a mutated variable into something like ta.sma(close, len), the answer is to make len an input.int(...) or a plain constant rather than a value you reassign. The length of a built-in moving average must be simple — it cannot be a series that changes each bar.
na means "no value," and Pine propagates it aggressively: almost any arithmetic touching na produces na. The errors here are rarely compile errors — they're runtime surprises where a plot goes blank, a condition never fires, or a counter stays stuck. The root cause is usually the first few bars of the chart (before an indicator has enough history) or a request.security call that hasn't returned data yet.
Two built-ins solve most of it. nz(x) replaces na with 0 (or a value you supply), and na(x) tests whether something is na so you can branch. A frequent mistake is writing if x == na — that comparison itself evaluates to na, not true, so the branch never runs. Always use the na(x) function for the test.
//@version=6
indicator("Safe na handling")
rsiVal = ta.rsi(close, 14)
// WRONG: `rsiVal == na` is itself na, never true
// RIGHT: use the na() function to test
plotColor = na(rsiVal) ? color.gray : (rsiVal > 70 ? color.red : color.teal)
// nz() gives a safe default for accumulation
var float runningSum = 0.0
runningSum := nz(runningSum) + nz(rsiVal, 0)
plot(rsiVal, color = plotColor)A subtler version bites people accumulating values: runningSum := runningSum + something will stay na forever if runningSum started as na, because na + anything = na. Wrapping the prior value in nz(...) breaks the chain. When a plot mysteriously "disappears" partway through history, suspect an na leaking in from a division by zero or an out-of-range historical reference.
Pine has several types — int, float, bool, color, string — plus the qualifiers const, input, simple, and series that describe when a value is known. The message "Cannot call 'X' with argument 'Y'=Z, it has type ..., expected ..." means the value you passed has the wrong base type or the wrong qualifier. Two cases dominate.
The base-type case: you passed a float where an int was required, e.g. a moving-average length computed by division. length arguments must be integers. A v6-specific gotcha makes this common: in Pine v6, dividing one integer by another yields a float when the values don't divide evenly (5 / 2 is 2.5), so an expression like baseLen / 2 is a float even though both operands are integers. Coerce it back with int(...) (which truncates) or math.round(...) first. Similarly, passing a number where a string timeframe is expected, or a series color into something wanting a const color, triggers the same family of messages.
//@version=6
indicator("Type conversion fix", overlay = true)
baseLen = input.int(20, "Base length")
// WRONG: in v6, baseLen / 2 is a float; ta.sma wants an int length
// RIGHT: coerce to int
halfLen = int(baseLen / 2)
fastMa = ta.sma(close, halfLen)
slowMa = ta.sma(close, baseLen)
plot(fastMa, color = color.orange)
plot(slowMa, color = color.blue)The qualifier case is sneakier: the base type is right (int is int), but you handed a series int to a parameter that demands simple int. This is exactly the "series vs simple" boundary, and it's common enough to deserve its own section next. When you see "expected a const/simple" in the error, you're in qualifier territory, not base-type territory — coercing with int() won't help, because the problem is when the value is known, not what it is.
A simple value is fixed before the script runs across history (an input, a constant, or a calculation built only from those). A series value can differ on every bar (close, a ta.* output, anything derived from them). Some built-ins — notably the length of ta.ema/ta.sma/ta.rsi, and the timeframe argument of request.security — require simple values, because they need to be known up front to allocate the calculation. Feed them a series and you get "Cannot call ... expected simple."
The fix is to source those arguments from inputs or constants, never from per-bar data. If you genuinely need a length that depends on market conditions, you generally have to precompute a small set of fixed lengths and select among their outputs, rather than passing a changing length into the function. That's a design constraint of Pine, not a bug to work around.
The other place this bites is request.security, where the real danger isn't a compile error but repainting — historical bars showing values that weren't actually available in real time. To request a higher timeframe without look-ahead bias, offset the series by [1] and pass lookahead = barmerge.lookahead_off, then gate any alert on barstate.isconfirmed so it only fires on closed bars:
//@version=6
indicator("Non-repainting HTF request", overlay = true)
tf = input.timeframe("D", "Higher timeframe") // simple string — required
len = input.int(14, "EMA length") // simple int — required
// offset by [1] + lookahead_off => no look-ahead, no repaint
htfEma = request.security(syminfo.tickerid, tf, ta.ema(close, len)[1], lookahead = barmerge.lookahead_off)
plot(htfEma, color = color.purple, linewidth = 2)
// only act on confirmed (closed) bars
crossUp = ta.crossover(close, htfEma)
alertcondition(crossUp and barstate.isconfirmed, "HTF cross up", "Price closed above HTF EMA")Getting length and timeframe as proper simple inputs clears the compile error; the [1] offset plus lookahead_off and the barstate.isconfirmed gate are what keep the script honest about what it could have known at the time. Tooling such as ForexCodes' Audit can flag these series/simple and look-ahead issues automatically, but the underlying habit is worth internalising: read the error for type versus qualifier, and default to non-repainting whenever you reach across timeframes.
When a script won't compile or behaves oddly, run through these in order. Most pine script errors resolve at one of the first few steps.
//@version=6 and that it uses namespaced built-ins (ta.sma, request.security, math.max) rather than v5-and-earlier bare calls (sma(), security(), study()). A surprising number of "undeclared identifier" errors are just stale syntax.= (or var ... =) to declare, and := to update. The phrase "cannot use a mutable variable" almost always points here, or to passing a reassigned value into a function wanting a fixed argument.int(...)/math.round(...) — and remember that in v6, int-by-int division can hand you a float. "expected a simple/const" means the value changes per bar and must instead come from an input or constant.na(x) function (never x == na), default with nz(x), and wrap accumulators so na can't poison the running total.request.security, use [1] and lookahead = barmerge.lookahead_off, and gate signals on barstate.isconfirmed.Clearing every error means your code compiles and runs deterministically — it does not mean the underlying idea has any edge. A clean compile and a backtest curve are not evidence of future results. Keep the engineering rigour for the code, and keep a healthy skepticism for the strategy. This article is educational only and is not financial advice; trading carries a genuine risk of loss.
Educational only — not financial advice. Trading involves substantial risk of loss.