Chapter 8 $ and [[ Operator Errors
What You’ll Learn:
- Differences between $, [[]], and []
- When each operator works (and fails)
- Recursive indexing in lists
- Partial matching pitfalls
- Atomic vector vs list extraction
Key Errors Covered: 12+ operator-specific errors
Difficulty: ⭐⭐ Intermediate
8.1 Introduction
R has three main extraction operators that confuse everyone:
my_list <- list(name = "Alice", age = 25, scores = c(85, 90, 95))
# Three ways to get the same thing:
my_list$name
#> [1] "Alice"
my_list[["name"]]
#> [1] "Alice"
my_list["name"] # Wait, this is different!
#> $name
#> [1] "Alice"# Check the types
class(my_list$name) # "character"
#> [1] "character"
class(my_list[["name"]]) # "character"
#> [1] "character"
class(my_list["name"]) # "list" - different!
#> [1] "list"Understanding these differences prevents endless frustration.
💡 Key Insight: The Three Operators
lst <- list(x = 1:3, y = "text", z = list(a = 10))
# $ - Extract by name (partial matching!)
lst$x # c(1, 2, 3)
#> [1] 1 2 3
lst$x # Same as lst[["x"]]
#> [1] 1 2 3
# [[ ]] - Extract single element (no partial matching)
lst[["x"]] # c(1, 2, 3)
#> [1] 1 2 3
lst[[1]] # Can use position too
#> [1] 1 2 3
# [ ] - Extract sub-list (keeps structure)
lst["x"] # list(x = 1:3)
#> $x
#> [1] 1 2 3
lst[1] # list(x = 1:3)
#> $x
#> [1] 1 2 3
lst[1:2] # list(x = 1:3, y = "text")
#> $x
#> [1] 1 2 3
#>
#> $y
#> [1] "text"Rule of thumb: - $ : Quick named access (interactive use) - [[]] : Safe programmatic access - [] : When you need to keep list structure
8.2 Error #1: $ operator is invalid for atomic vectors
⭐ BEGINNER 🔢 TYPE
8.2.1 The Error
x <- c(a = 1, b = 2, c = 3) # Named vector
x$a # $ doesn't work on vectors!
#> Error in x$a: $ operator is invalid for atomic vectors🔴 ERROR
Error in x$a : $ operator is invalid for atomic vectors
8.2.2 What It Means
The $ operator only works on recursive objects (lists, data frames, environments). It doesn’t work on atomic vectors.
8.2.3 Understanding the Difference
# Atomic vector (1D, all same type)
vec <- c(a = 1, b = 2, c = 3)
is.atomic(vec) # TRUE
#> [1] TRUE
is.recursive(vec) # FALSE
#> [1] FALSE
# Use names indexing instead
vec["a"]
#> a
#> 1
vec[["a"]]
#> [1] 1
# List (recursive)
lst <- list(a = 1, b = 2, c = 3)
is.atomic(lst) # FALSE
#> [1] FALSE
is.recursive(lst) # TRUE
#> [1] TRUE
# $ works here
lst$a
#> [1] 18.2.4 Common Causes
8.2.5 Solutions
✅ SOLUTION 1: Use Correct Indexing for Vectors
✅ SOLUTION 2: Convert to List if Needed
✅ SOLUTION 3: Check Object Type First
safe_dollar <- function(x, name) {
if (is.atomic(x) && !is.null(names(x))) {
message("Using [[ ]] for atomic vector")
return(x[[name]])
} else if (is.recursive(x)) {
return(x[[name]])
} else {
stop("Cannot extract '", name, "' from ", class(x)[1])
}
}
# Test
vec <- c(a = 1, b = 2)
safe_dollar(vec, "a")
#> Using [[ ]] for atomic vector
#> [1] 1
lst <- list(a = 1, b = 2)
safe_dollar(lst, "a")
#> [1] 1⚠️ Common Pitfall: Data Frame Columns
df <- data.frame(x = 1:3, y = 4:6, z = 7:9)
# Extracting a column
col <- df$x # Vector (atomic)
col$something # Error!
#> Error in col$something: $ operator is invalid for atomic vectors
# Selecting columns keeps data frame
subset <- df["x"] # Data frame (recursive)
subset$x # Works!
#> [1] 1 2 3
# Rule: Single column with $ or [[ ]] → vector
# One or more columns with [ ] → data frame8.3 Error #2: recursive indexing failed at level X
⭐⭐ INTERMEDIATE 📏 DIMENSION
8.3.1 The Error
my_list <- list(a = list(b = 1))
my_list[[c("a", "b", "c")]] # Too deep!
#> Error in my_list[[c("a", "b", "c")]]: subscript out of bounds🔴 ERROR
Error in my_list[[c("a", "b", "c")]] :
recursive indexing failed at level 3
8.3.3 Recursive Indexing
8.3.5 Solutions
✅ SOLUTION 1: Check Path Exists
nested <- list(a = list(b = list(c = 42)))
safe_deep_extract <- function(x, path) {
for (i in seq_along(path)) {
if (!is.list(x) && !is.environment(x)) {
stop("Cannot recurse at level ", i,
": object is ", class(x)[1], ", not list")
}
if (!path[i] %in% names(x)) {
stop("Name '", path[i], "' not found at level ", i)
}
x <- x[[path[i]]]
}
return(x)
}
# Test
safe_deep_extract(nested, c("a", "b", "c")) # Works
#> [1] 42✅ SOLUTION 2: Use purrr::pluck()
✅ SOLUTION 3: Step-by-Step Extraction
nested <- list(a = list(b = list(c = 42)))
path <- c("a", "b", "c")
# Extract step by step with checking
result <- nested
for (step in path) {
if (is.null(result)) {
message("Path ended at NULL")
break
}
if (!step %in% names(result)) {
message("'", step, "' not found")
result <- NULL
break
}
result <- result[[step]]
}
result
#> [1] 428.4 Error #3: attempt to select less than one element
⭐⭐ INTERMEDIATE 📏 DIMENSION
8.4.1 The Error
my_list <- list(a = 1, b = 2)
my_list[[integer(0)]] # Empty index!
#> Error in my_list[[integer(0)]]: attempt to select less than one element in get1index🔴 ERROR
Error in my_list[[integer(0)]] :
attempt to select less than one element in integerOneIndex
8.4.4 Solutions
✅ SOLUTION 1: Check Before Extracting
✅ SOLUTION 2: Use [ ] for Multiple/Zero Elements
✅ SOLUTION 3: Safe Extraction Function
safe_extract_one <- function(x, i, default = NULL) {
if (length(i) == 0) {
message("No index provided")
return(default)
}
if (is.na(i)) {
message("Index is NA")
return(default)
}
if (i < 1 || i > length(x)) {
message("Index out of bounds: ", i)
return(default)
}
return(x[[i]])
}
# Test
my_list <- list(a = 1, b = 2)
safe_extract_one(my_list, integer(0))
#> No index provided
#> NULL
safe_extract_one(my_list, 1)
#> [1] 1
safe_extract_one(my_list, 10)
#> Index out of bounds: 10
#> NULL8.5 Error #4: attempt to select more than one element
⭐ BEGINNER 📏 DIMENSION
8.5.1 The Error
my_list <- list(a = 1, b = 2, c = 3)
my_list[[c(1, 2)]] # Multiple indices!
#> Error in my_list[[c(1, 2)]]: subscript out of bounds🔴 ERROR
Error in my_list[[c(1, 2)]] :
attempt to select more than one element in integerOneIndex
8.5.3 Single vs Multiple Selection
my_list <- list(a = 1, b = 2, c = 3)
# [[ ]] - One element
my_list[[1]] # Element 1
#> [1] 1
my_list[["a"]] # By name
#> [1] 1
# [ ] - Multiple elements (returns list)
my_list[1] # List with element 1
#> $a
#> [1] 1
my_list[1:2] # List with elements 1 and 2
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
my_list[c("a", "c")] # By names
#> $a
#> [1] 1
#>
#> $c
#> [1] 38.5.4 Common Causes
8.5.4.2 Cause 2: Vector of Names
8.5.5 Solutions
✅ SOLUTION 1: Use [ ] for Multiple Elements
✅ SOLUTION 2: Loop or Apply for Multiple
data <- list(x = 1:3, y = 4:6, z = 7:9)
elements_wanted <- c("x", "z")
# Extract each separately
result <- lapply(elements_wanted, function(name) data[[name]])
names(result) <- elements_wanted
result
#> $x
#> [1] 1 2 3
#>
#> $z
#> [1] 7 8 9
# Or use [ ] and unlist if needed
data[elements_wanted]
#> $x
#> [1] 1 2 3
#>
#> $z
#> [1] 7 8 98.6 Partial Matching with $
⚠️ Dangerous Pitfall: Partial Matching
The $ operator does partial matching by default:
my_list <- list(name = "Alice", age = 25)
# Exact match
my_list$name
#> [1] "Alice"
# Partial match (DANGEROUS!)
my_list$n # Matches "name"
#> Warning in my_list$n: partial match of 'n' to 'name'
#> [1] "Alice"
my_list$na # Matches "name"
#> Warning in my_list$na: partial match of 'na' to 'name'
#> [1] "Alice"
my_list$nam # Matches "name"
#> Warning in my_list$nam: partial match of 'nam' to 'name'
#> [1] "Alice"
# Ambiguous partial match returns NULL
my_list$a # Could be "age" - but only one letter, returns NULL
#> Warning in my_list$a: partial match of 'a' to 'age'
#> [1] 25
# [[ ]] does NOT partial match (SAFER)
my_list[["n"]] # NULL
#> NULL
my_list[["name"]] # Works
#> [1] "Alice"Best Practice: Use [[]] in production code to avoid partial matching surprises.
8.7 Comparing Operators
💡 Key Insight: Complete Comparison
lst <- list(x = 1:3, y = "text", z = list(a = 10, b = 20))
# Extraction comparison
lst$x # c(1, 2, 3) - vector
#> [1] 1 2 3
lst[["x"]] # c(1, 2, 3) - vector
#> [1] 1 2 3
lst["x"] # list(x = c(1, 2, 3)) - list
#> $x
#> [1] 1 2 3
# Type returned
class(lst$x) # "integer"
#> [1] "integer"
class(lst[["x"]]) # "integer"
#> [1] "integer"
class(lst["x"]) # "list"
#> [1] "list"
# Multiple elements
# lst$c("x", "y") # Can't do this
# lst[[c("x", "y")]] # Error
lst[c("x", "y")] # Works - returns list
#> $x
#> [1] 1 2 3
#>
#> $y
#> [1] "text"
# Nested access
# lst$z$a # 10
lst[["z"]]$a # 10
#> [1] 10
lst[["z"]][["a"]] # 10
#> [1] 10
lst[[c("z", "a")]] # 10 (recursive indexing)
#> [1] 10
# lst[c("z", "a")] # list with both z and a (different!)Decision Tree:
Need to extract from list/data frame?
├─ One element?
│ ├─ Known name, interactive? → use $
│ ├─ Programmatic, exact name? → use [[]]
│ └─ By position? → use [[]]
└─ Multiple elements?
└─ Use []
8.8 Data Frame Special Cases
🎯 Best Practice: Data Frame Extraction
df <- data.frame(x = 1:3, y = 4:6, z = 7:9)
# Column extraction
df$x # Vector (drops to 1D)
#> [1] 1 2 3
df[["x"]] # Vector (drops to 1D)
#> [1] 1 2 3
df["x"] # Data frame (keeps 2D)
#> x
#> 1 1
#> 2 2
#> 3 3
df[, "x"] # Vector (drops by default)
#> [1] 1 2 3
df[, "x", drop = FALSE] # Data frame (preserved)
#> x
#> 1 1
#> 2 2
#> 3 3
# Multiple columns
# df$c("x", "y") # Can't do
# df[[c("x", "y")]] # Error
df[c("x", "y")] # Data frame with 2 columns
#> x y
#> 1 1 4
#> 2 2 5
#> 3 3 6
df[, c("x", "y")] # Data frame with 2 columns
#> x y
#> 1 1 4
#> 2 2 5
#> 3 3 6
# With dplyr (clearest!)
library(dplyr)
df %>% pull(x) # Vector
#> [1] 1 2 3
df %>% select(x) # Data frame
#> Error in select(., x): unused argument (x)
df %>% select(x, y) # Data frame
#> Error in select(., x, y): unused arguments (x, y)Rule:
- $ and [[]] → Drop to vector (single column)
- [] → Keep as data frame
- [, , drop = FALSE] → Force data frame
8.9 Summary
Key Takeaways:
- $ only works on lists/data frames - Not atomic vectors
- [[ ]] requires exact names - No partial matching
- [[ ]] extracts one element - Use [] for multiple
- Recursive indexing - [[c(“a”, “b”)]] goes deep
- $ does partial matching - Dangerous, use [[ ]] in code
- [] keeps structure - Returns list/data frame
- [[ ]] and $ simplify - Return element itself
Quick Reference:
| Operator | Structure | Elements | Partial Match | Use Case |
|---|---|---|---|---|
$ |
List/DF | One | Yes | Interactive |
[[]] |
Any | One | No | Programmatic |
[] |
Any | Multiple | No | Subsetting |
Common Errors:
| Error | Cause | Fix |
|---|---|---|
| $ invalid for atomic vectors | Used $ on vector | Use [[ ]] or [] |
| recursive indexing failed | Path too deep | Check structure |
| select less than one | Empty index in [[ ]] | Check length first |
| select more than one | Multiple indices in [[ ]] | Use [] instead |
Best Practices:
8.10 Exercises
📝 Exercise 1: Operator Selection
Which operator(s) work for each scenario?
📝 Exercise 2: Debug the Extraction
Fix these extraction errors:
# Problem 1
numbers <- c(x = 10, y = 20, z = 30)
result <- numbers$x
# Problem 2
data <- list(values = c(1, 2, 3))
item <- data[[c("values", "1")]]
# Problem 3
my_list <- list(a = 1, b = 2, c = 3)
subset <- my_list[[c(1, 3)]]
# Problem 4
nested <- list(level1 = list(level2 = 10))
value <- nested$level1$level2$level3📝 Exercise 3: Safe Accessor
Write safe_get(x, path, default = NULL) that:
1. Works with nested lists
2. Handles missing names gracefully
3. Returns default if path doesn’t exist
4. Works with both character names and numeric indices
5. Provides helpful error messages
📝 Exercise 4: Extraction Comparison
For this structure, show what each extraction returns:
8.11 Exercise Answers
Click to see answers
Exercise 1:
vec <- c(a = 1, b = 2)
lst <- list(a = 1, b = 2)
df <- data.frame(a = 1:3, b = 4:6)
# A: Get "a" from vector
vec["a"] # ✅ Works
#> a
#> 1
vec[["a"]] # ✅ Works
#> [1] 1
# vec$a # ❌ Error (atomic vector)
# B: Get "a" from list
lst$a # ✅ Works
#> [1] 1
lst[["a"]] # ✅ Works
#> [1] 1
lst["a"] # ✅ Works (but returns list)
#> $a
#> [1] 1
# C: Get column "a" as vector
df$a # ✅ Works
#> [1] 1 2 3
df[["a"]] # ✅ Works
#> [1] 1 2 3
df[, "a"] # ✅ Works
#> [1] 1 2 3
# df["a"] # ❌ Returns data frame, not vector
# D: Get column "a" as data frame
df["a"] # ✅ Works
#> a
#> 1 1
#> 2 2
#> 3 3
df[, "a", drop = FALSE] # ✅ Works
#> a
#> 1 1
#> 2 2
#> 3 3
# df$a # ❌ Returns vector
# df[["a"]] # ❌ Returns vector
# E: Get multiple elements from list
lst[c("a", "b")] # ✅ Only this works
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
# lst$c("a", "b") # ❌ Syntax error
# lst[[c("a", "b")]] # ❌ ErrorExercise 2:
# Problem 1 - $ on atomic vector
numbers <- c(x = 10, y = 20, z = 30)
result <- numbers[["x"]] # or numbers["x"]
# Problem 2 - Can't recurse into atomic vector
data <- list(values = c(1, 2, 3))
item <- data[["values"]][1] # or data$values[1]
# Problem 3 - Multiple indices in [[]]
my_list <- list(a = 1, b = 2, c = 3)
subset <- my_list[c(1, 3)] # Use single bracket
# Problem 4 - Path doesn't exist
nested <- list(level1 = list(level2 = 10))
# Check if exists first
if (!is.null(nested$level1$level2)) {
value <- nested$level1$level2
} else {
value <- NA
}
# Or just:
value <- nested$level1$level2 # This is 10
# nested$level1$level2$level3 would be NULLExercise 3:
safe_get <- function(x, path, default = NULL) {
# Handle empty path
if (length(path) == 0) {
return(x)
}
# Iterate through path
current <- x
for (i in seq_along(path)) {
step <- path[i]
# Check if current is indexable
if (!is.list(current) && !is.environment(current)) {
message("Cannot index into ", class(current)[1], " at step ", i)
return(default)
}
# Check if step exists
if (is.character(step)) {
if (!step %in% names(current)) {
message("Name '", step, "' not found at step ", i)
return(default)
}
current <- current[[step]]
} else if (is.numeric(step)) {
if (step < 1 || step > length(current)) {
message("Index ", step, " out of bounds at step ", i)
return(default)
}
current <- current[[step]]
} else {
stop("Path element must be character or numeric")
}
}
return(current)
}
# Test
nested <- list(a = list(b = list(c = 42)))
safe_get(nested, c("a", "b", "c")) # 42
#> [1] 42
safe_get(nested, c("a", "b", "c", "d")) # NULL
#> Cannot index into numeric at step 4
#> NULL
safe_get(nested, c("a", "x"), default = NA) # NA
#> Name 'x' not found at step 2
#> [1] NA
safe_get(nested, c(1, 1, 1)) # 42 (by index)
#> [1] 42Exercise 4:
data <- list(
user = list(
name = "Alice",
scores = c(85, 90, 95)
)
)
# data$user
# Returns: list(name = "Alice", scores = c(85, 90, 95))
# Type: list
# data[["user"]]
# Returns: list(name = "Alice", scores = c(85, 90, 95))
# Type: list
# data["user"]
# Returns: list(user = list(name = "Alice", scores = c(85, 90, 95)))
# Type: list (wrapped in another list!)
# data$user$name
# Returns: "Alice"
# Type: character
# data[[c("user", "name")]]
# Returns: "Alice"
# Type: character
# data[["user"]][["scores"]][[2]]
# Returns: 90
# Type: numeric
# Show them
data$user
#> $name
#> [1] "Alice"
#>
#> $scores
#> [1] 85 90 95
data[["user"]]
#> $name
#> [1] "Alice"
#>
#> $scores
#> [1] 85 90 95
data["user"]
#> $user
#> $user$name
#> [1] "Alice"
#>
#> $user$scores
#> [1] 85 90 95
data$user$name
#> [1] "Alice"
data[[c("user", "name")]]
#> [1] "Alice"
data[["user"]][["scores"]][[2]]
#> [1] 90