The Dart RangeError Trap: Secure Your List Access with elementAtOrNull

Your Flutter app feels solid. You’ve embraced null safety, sprinkling ?. where needed. Then, production crashes start rolling in —…

Back to all articles

Your Flutter app feels solid. You’ve embraced null safety, sprinkling ?. where needed. Then, production crashes start rolling in — RangeError: Invalid value.

This isn’t theoretical; in our own codebase at Saropa, a routine debug session uncovered this exact RangeError despite extensive null-safety checks, lurking where we hadn’t anticipated.

This guide fixes a common blind spot: null safety alone doesn’t save you from invalid list index access. We’ll show you why it happens and present the definitive best practice using elementAtOrNull to make your code truly robust.

_________________________________________
|                                         |
|  List? data = fetchData();      | // Could be null, [], ['a'], ['a', 'b']
|                                         |
|  // Common but UNSAFE attempt           |
|  String value = data?[1].toUpperCase(); | // <-- Expected RangeError💥
|                                         |
|_________________________________________|
   \___________________________________/
       \__ The Hidden Danger Zone __/

Important Note: This guide is for developers building production Dart/Flutter applications who prioritize stability. We’re focusing on the standard, safe way to handle dynamic list data.

The Common Pitfall: Misplaced Trust in ?[]

Developers see ?[] and often think it magically handles all list access problems. It doesn’t. The danger lies in assuming it protects against invalid indices when the list does exist.

  • What ?[] Does: It only checks if the list itself is null. If null, the expression short-circuits to null.
  • What ?[] Doesn’t Do: If the list is not null (even if empty), ?[] allows the standard index access [] to proceed.
  • The Crash: Accessing list[index] with an out-of-bounds index (negative or >= list.length) throws an immediate RangeError.

Consider this (unsafe) scenario:

// Fetching user roles or permissions
List? userRoles = await getCurrentUserRoles();

// Attempting to get the secondary role's length (UNSAFE)
// We use '?.' correctly after '[1]', but the RangeError happens
//     at '[1]' if userRoles has < 2 elements!
int? secondaryRoleLength = userRoles?[1]?.length; // <-- RangeError happens at '[1]' evaluation

// This line isn't reached if RangeError occurs
print('Secondary Role Length: $secondaryRoleLength');

In the line userRoles?[1]?.length;:

  1. userRoles? checks if userRoles is null. If yes, the whole thing becomes null. Safe so far.
  2. If userRoles is NOT null (e.g., [‘Admin’]), it proceeds to evaluate userRoles[1].
  3. CRASH POINT: Since userRoles only has length 1, index 1 is invalid. The evaluation of userRoles[1] throws a RangeError before the subsequent ?.length is even considered.

When you attempt to access a list element using the direct index access operator `[]`, the Dart runtime performs a check to ensure the provided index is within the valid bounds of the list (0 to `list.length — 1`). If the index falls outside these bounds (either negative or greater than or equal to the list’s length), a `RangeError` is immediately thrown. This happens synchronously during the access attempt.

The critical misunderstanding is that ?[] validates the index. It does not. It solely prevents a NoSuchMethodError on a null list object itself. The responsibility of ensuring the index is valid if the list exists remains.

Why Linters Don’t Warn You

Standard Dart linters excel at static analysis — checking types and nullability without running the code. They can’t predict the runtime length of your userRoles list, which might change based on the user, API responses, or other dynamic factors. So, userRoles?[1] looks syntactically valid, and the linter stays quiet.

(Misleading Suggestion) Focus on Type Nullability

Worse still, if your list contains non-nullable elements (e.g., List<String>), and you defensively write list[index]?.someMethod(), the linter might actually issue a warning like invalid_null_aware_operator.

It suggests removing the ?. and using just . because, if the index access list[index] were to succeed, the resulting element (String in this case) cannot be null according to its type. The linter, focused on the type system, doesn’t account for the preceding list[index] potentially throwing a RangeError before someMethod is ever reached.

This suggestion, while technically correct about the element’s nullability post-access, inadvertently encourages removing a perceived safeguard and masks the real underlying risk of the RangeError during the index access itself.

The Best Practice: elementAtOrNull from package:collection

Forget manual length checks that clutter your code. The official package:collection provides the standard toolkit for robust collection handling.

Its elementAtOrNull method is the definitive solution. Unlike direct [] access, elementAtOrNull(index) safely returns the element at index. It avoids errors by returning null whenever the list is null or the index is invalid.

Refactoring the Pitfall:

import 'package:flutter/material.dart';

List? userRoles = await getCurrentUserRoles();

// Safe access using elementAtOrNull
// elementAtOrNull(1) returns null if userRoles is null OR index 1 is invalid.
// The subsequent '?.length' correctly handles this potential null.
int? secondaryRoleLength = userRoles?.elementAtOrNull(1)?.length;

// No crash! Prints 'null' if index 1 is unavailable.
print('Secondary Role Length: $secondaryRoleLength');

This integrates cleanly with ?. and ??. If elementAtOrNull(1) returns null, subsequent ?. calls short-circuit correctly, and ?? provides a default if needed.

Performance Considerations

Experienced Flutter developers are naturally concerned with performance. Direct list access (list[index]) in Dart is a very efficient O(1) operation for standard List implementations, involving a quick bounds check. However, triggering a RangeError incurs a performance cost due to exception handling’s overhead.

Direct list access (list[index]) is fast if the index is valid, but out-of-bounds access leads to RangeError which may have a significant performance cost and cause user-affecting display issues.

Strategy for Production Codebases 🔧

Fixing potentially hundreds of list[index] instances requires a plan:

  1. Mandate the Standard: Enforce via code reviews that all new code and any modified code involving list index access MUST use elementAtOrNull. No exceptions.
  2. Identify Existing Crashes: Use your production crash reporting (Firebase Crashlytics, Sentry, etc.) to find the exact lines throwing RangeError. Fix these high-priority spots first.
  3. Refactor Incrementally: When working on a feature or bug, if you touch a file with unsafe list[index] access, refactor it to elementAtOrNull as part of the task.
  4. Targeted Search (Use With Caution): Use IDE search with regex for unsafe chaining after index access
RegEx example (ignoring inline comments):

(\w+\??\[[^\]]+\])\s*(?

5. Review: Manually check every match before refactoring — regex can’t
understand full context.

This procedure accurately portrays the Saropa experience as a proactive discovery during development that highlighted the scale of the potential problem (173 instances!) and triggered preventative action.


Conclusion: Build for Reliability

Adopting `elementAtOrNull` as the standard for accessing list elements at potentially out-of-bounds indices yields significant benefits for experienced Flutter development teams:

  • Increased Application Stability Directly reduces the occurrence of `RangeError` crashes in production.
  • Improved Code Readability: Explicitly signals the possibility of an absent element at a given index.
  • Reduced Cognitive Load: Eliminates the need for manual `length` checks in many common scenarios.
  • More Robust Code: Handles dynamic list lengths gracefully, making the application less brittle to data changes.
  • Alignment with Best Practices: Encourages a more defensive programming style.

Don’t let the RangeError trap catch you or your users!


References:

Final Word 🪅

Illustration from article
saropa.com
Share this article

Your feedback is essential to us, and we genuinely value your support. When we learn of a mistake, we acknowledge it with a correction. If you spot an error, please let us know at blog@saropa.com and learn more at saropa.com.

Originally published by Saropa on Medium on March 27, 2025. Copyright © 2025