R Error: 'replacement has length zero', The Hidden NA That Breaks Assignment
Error in x[i] <- value : replacement has length zero means the right side of your assignment evaluated to nothing, a NULL, an empty vector, or a missing index, so R has no value to place into x[i]. It almost always traces back to a filter, lookup, or subscript that silently returned zero rows.
What does "replacement has length zero" actually mean?
The error fires when R tries to execute x[i] <- value and finds that value has length zero. The assignment slot still expects one concrete element, so R refuses and throws. The tricky part is that the right-hand side rarely looks empty, it came from a filter, a grep(), a which(), or a lookup whose key was not in the table. Reproducing the failure in a familiar for loop makes the mechanism obvious.
Reproduce it with a short for loop that pulls ages from a lookup table, two of the three keys match, one does not:
The loop runs fine for Alice (i = 1) because ages_table$age[ages_table$name == "Alice"] returns 30, a length-1 vector that fits neatly into ages[1]. On Bob (i = 2) the same expression returns numeric(0) because no row matches. ages[2] <- numeric(0) asks R to put zero values into one slot, which is impossible, and the loop dies with the exact error message above. The fix is never to "catch" the error, it is to prevent the RHS from being empty in the first place.
Try it: Rewrite the loop so it fills ex_ages[i] with NA_real_ when a name is not in ages_table. Use a length(...) > 0 guard so the loop never crashes.
Click to reveal solution
Explanation: The guard collapses the hit vector to a single value before touching ex_ages[i]. If the lookup missed, length(hit) is 0 and the else branch supplies NA_real_. The slot now always receives exactly one number, so the assignment is legal.
Which RHS patterns silently produce a zero-length result?
Four common R idioms return a zero-length vector whenever they fail to find anything, and all four are happy to feed that emptiness into your next assignment. Knowing them by name turns "mysterious crash" into "ah, it's pattern number three".
Every call above returns a zero-length vector of the appropriate type: integer(0), integer(0), numeric(0), numeric(0). None of them raise a warning, and none print anything unusual, they look like normal return values right up until you try to place them into a single slot. Patterns 1 and 2 return integer(0) because both grep() and which() are indexing functions. Patterns 3 and 4 preserve the storage mode of the source vector, so the emptiness "looks" like the numeric type you expected.
length(rhs). When you see "replacement has length zero", isolate the exact expression on the right side of the failing <-, wrap it in length(), and print it. If the answer is 0, you have found the cause, no further digging required.Try it: Fill ex_hits[i] with the number of grep() matches for each query. The second query ("melon") matches nothing, make sure the loop does not crash on it.
Click to reveal solution
Explanation: Writing length(found) into the slot always produces a single integer, so the assignment is never empty. A common broken version is ex_hits[i] <- found, when found is integer(0), that form triggers "replacement has length zero" on the "melon" iteration, which is exactly the error this post is about.
How do you diagnose a zero-length assignment before R does?
Reactive debugging, waiting for the crash and then reading the traceback, is painful because the error message never names the key that was missing. Proactive diagnostics fail loud at the exact line where the RHS goes empty, and they name the culprit.
The workhorse is a hand-rolled stop() that names the culprit. Wrap your lookup in a tiny function that asserts exactly one row came back, and you get an error message that points at the data, not the assignment.
Compare that to the vague "replacement has length zero" message you started with. The new failure reads as no row for key: Bob, tells you exactly which key broke the contract, and suppresses the function-call prefix with call. = FALSE so the message stays clean. For loops that process thousands of keys, this turns a ten-minute hunt into a one-line fix, you know which key is missing because the loop dies on it before the assignment even runs.
sapply() and vapply(). When a zero-length error blows up inside an apply family call, the traceback points at the outer call, not the iteration that failed. Isolate the failing key with a plain for loop first, fix the RHS, then vectorise.Try it: Finish the function so it asserts its lookup returned exactly one value and produces a readable message on failure.
Click to reveal solution
Explanation: A hand-rolled stop() gives you the freedom to interpolate the failing key into the error message. call. = FALSE suppresses the function-call prefix so the message is clean. Now any downstream caller sees exactly which key is missing instead of chasing a generic replacement-length error.
Which defensive patterns prevent the error in production code?
Diagnostics are for debugging. Defensive patterns are for code you want to stop worrying about. Three patterns cover the vast majority of production cases, and all three are one line each.
The first is %||%, a null-coalesce operator that returns its left operand unless it is NULL or zero-length, in which case it returns the right operand as a fallback. This is the same || you use in JavaScript, except R needs a custom helper.
Every call to %||% collapses an uncertain RHS into a guaranteed length-1 result, so the downstream assignment can never error. Notice how the same expression behaves in both the matched and unmatched case, that uniformity is what makes this pattern production-ready.
rlang::%||% ships in the tidyverse. If your project already depends on rlang, dplyr, or ggplot2, you do not need to redefine the operator. Just write library(rlang) at the top of your script and use %||% directly.The second pattern replaces the entire for loop with a vectorised match(). This is almost always the right answer, R's indexing is built for it.
match() returns NA_integer_ for every key that is not in the table. When you use that NA as an index, R quietly returns NA from the lookup vector, no zero-length intermediate, no crash, no stopifnot() needed. A three-line vectorised pipeline replaces a ten-line guarded loop and runs faster on real data sizes.
match() is almost always the right answer. for loops with scalar lookups are where zero-length errors breed. Vectorised indexing never returns integer(0), it returns NA in the right positions, which is a legal assignment.Try it: Build a safe_first() helper that returns the first element of a vector, or NA_real_ if the vector is empty or NULL.
Click to reveal solution
Explanation: The early return collapses both the NULL and zero-length cases into a single NA_real_ result. The happy path uses [[1]] (double bracket) to unwrap the first element cleanly, x[1] would keep vector names, which is usually not what you want in a scalar helper.
Practice Exercises
Exercise 1: Safe user age lookup
You have a vector of user ids and a data frame of known users. Build an ages vector that contains the user's age for every matched id and NA_real_ for every unmatched id. The solution must be vectorised, no for loop, and must never raise "replacement has length zero".
Click to reveal solution
Explanation: match() walks cap_users once and produces a length-5 integer vector of row positions, using NA_integer_ for the two unmatched ids. Indexing cap_users_df$age by that vector preserves the length, zero-length slices cannot appear because every NA index resolves to NA in the result.
Exercise 2: Audit wrapper with missing-key logging
A log-processing function is supposed to enrich each event with a human-readable category pulled from a lookup table. When a category is missing, the function currently crashes. Refactor it so that it (a) never errors, (b) returns a character vector of categories with NA for misses, and (c) carries an attribute "missing_keys" listing every unresolved key for auditing.
Click to reveal solution
Explanation: match() produces NA for every unknown code, so indexing lookup$category returns NA in those positions instead of crashing. events[is.na(idx)] picks out the raw keys that failed, and unique() collapses duplicates so the audit trail is clean. Storing the result as an attribute keeps the function's return type a plain character vector while still giving auditors the visibility they need.
Complete Example
A realistic "order enrichment" pipeline: every order references a customer id, and you want to attach the customer's city to each order. Some orders reference customers that were deleted. The naive approach crashes; the vectorised approach never does and also tells you which customers are missing.
The whole pipeline is four lines of real work plus an audit line. No for loop. No tryCatch. No "replacement has length zero". The two deleted customers surface as NA in the city column, which downstream code can handle with is.na() checks, and the missing_cust vector gives the operations team a list of data-quality issues to fix upstream. This is the shape almost every real enrichment job should take.
Summary
| Cause | How it shows up | Fix | ||
|---|---|---|---|---|
| Filter with no matches | df$col[df$key == x] returns length-0 |
match(x, df$key) + index |
||
grep() no match |
grep("...", v) returns integer(0) |
length(hits) > 0 guard or `[1] %\ |
\ | % NA` |
which() on always-false logical |
which(cond) returns integer(0) |
Same guard pattern | ||
Function with no else branch |
Silent NULL return |
Always write the else clause |
||
NA index from match() |
Slices propagate NA, not length-0 |
Assign through the NA, no fix needed |
||
| Loop scalar lookup | Any of the above, one iteration at a time | Replace loop with vectorised match() |
References
- R Core Team, The R Language Definition, section on "Indexing" and "Subset assignment". Link
- Wickham, H., Advanced R, 2nd Edition, Chapter 4: Subsetting. Link
- R documentation,
?base::match(returnsNAfor unmatched, never length-0). Link - R documentation,
?base::which(returnsinteger(0)when no element is TRUE). Link - rlang reference,
%||%null-default operator. Link - dplyr reference,
coalesce()for replacing missing values in vectors. Link
Continue Learning
- R Common Errors, the full reference for the 50 most common R error messages, including this one and its close cousins.
- R Error: object 'x' not found, the paired companion error that shows up when the left side of an assignment points at something that does not exist.
- R Error: subscript out of bounds, the closest conceptual cousin, fired when an index exceeds the length of the vector instead of collapsing to zero.