string.index(string.startIndex, offsetBy: n) to get a Character, or add a String extension for convenient Int-based access that handles bounds safely.Why this happens
Swift strings are collections of extended grapheme clusters—what users perceive as characters—rather than fixed-width code units. Because Unicode characters can occupy different numbers of bytes (UTF-8) or code units (UTF-16), Swift cannot guarantee O(1) random access by integer index. The compiler therefore emits the error:
'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
Apple’s documentation explains that “the concept of ‘the ith character in a string’ has different interpretations” depending on whether you mean UTF-8 code units, UTF-16 code units, Unicode scalars, or grapheme clusters. To avoid ambiguity, Swift forces you to choose the correct view explicitly.
How to fix it
1. Use the standard library (no extensions)
For one-off access, use String.Index arithmetic. This is the safest, most explicit approach and requires no extra code.
let str = "Hello, world!"
let index = str.index(str.startIndex, offsetBy: 7) // 0-based
let char: Character = str[index] // 'w'
print(char)
To get a String instead of a Character, wrap the result:
let substring = str[index...] // Substring "world!"
let string = String(substring) // "world!"
Bounds safety: offsetBy: traps at runtime if the offset is out of range. Guard first:
func character(at offset: Int, in string: String) -> Character? {
guard offset >= 0, offset < string.count else { return nil }
let idx = string.index(string.startIndex, offsetBy: offset)
return string[idx]
}
2. Add a convenient StringProtocol extension (recommended for projects)
If you frequently need integer-based access, extend StringProtocol once and reuse it everywhere. This version returns Character for single indices and SubSequence (Substring) for ranges, preserving the original string’s storage.
extension StringProtocol {
subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
subscript(range: Range<Int>) -> SubSequence {
let start = index(startIndex, offsetBy: range.lowerBound)
return self[start..<index(start, offsetBy: range.count)]
}
subscript(range: ClosedRange<Int>) -> SubSequence {
let start = index(startIndex, offsetBy: range.lowerBound)
return self[start..<index(start, offsetBy: range.count)]
}
subscript(range: PartialRangeFrom<Int>) -> SubSequence {
self[index(startIndex, offsetBy: range.lowerBound)...]
}
subscript(range: PartialRangeThrough<Int>) -> SubSequence {
self[...index(startIndex, offsetBy: range.upperBound)]
}
subscript(range: PartialRangeUpTo<Int>) -> SubSequence {
self[..<index(startIndex, offsetBy: range.upperBound)]
}
}
Usage:
let s = "Hello, world!"
s[0] // 'H' (Character)
s[0..<5] // "Hello" (Substring)
s[7...] // "world!" (Substring)
String(s[0..<5]) // "Hello" (String) — only when you need to store long-term
Note: These subscripts still trap on out-of-bounds offsets. Add guards if your indices come from untrusted input.
3. Safe, clamping String extension (forgiving API)
If you prefer an API that never crashes and clamps indices to valid bounds—similar to the high-scoring Stack Overflow answer—add this String-only extension:
extension String {
var length: Int { count }
subscript(i: Int) -> String {
self[i..<i+1]
}
func substring(fromIndex: Int) -> String {
self[min(fromIndex, length)..<length]
}
func substring(toIndex: Int) -> String {
self[0..<max(0, toIndex)]
}
subscript(r: Range<Int>) -> String {
let clamped = Range(uncheckedBounds: (
lower: max(0, min(length, r.lowerBound)),
upper: min(length, max(0, r.upperBound))
))
let start = index(startIndex, offsetBy: clamped.lowerBound)
let end = index(start, offsetBy: clamped.upperBound - clamped.lowerBound)
return String(self[start..<end])
}
}
Usage:
let str = "abcdef"
str[1..<3] // "bc"
str[5] // "f"
str[80] // "" (empty, no crash)
str.substring(fromIndex: 3) // "def"
str.substring(toIndex: 4) // "abcd"
This version returns String directly, which is convenient but allocates a new string each time. For high-performance loops, prefer the StringProtocol extension that returns SubSequence.
How to prevent it
- Default to
String.Indexfor new code; it makes the Unicode complexity visible. - Centralize integer subscripts in a single extension file so the team uses one consistent, tested implementation.
- Avoid character-by-character processing on user-facing text. Use
localizedStandardCompare,localizedCaseInsensitiveContains,enumerateSubstrings(in:options:using:), orString.Indexiteration instead. - Unit-test edge cases: empty strings, emoji sequences (e.g., “👨👩👧👦”), combining marks, and indices at
countandcount - 1.
FAQ
Why does string[0] work in Python but not Swift?
Python 3 strings are sequences of Unicode code points (fixed 21-bit values), so integer indexing is O(1). Swift strings are UTF-8 internally and expose grapheme clusters, which require variable-length decoding. Swift prioritizes correctness over convenience for human-readable text.
Can I use Array(string) to get random access?
Yes—Array(string) gives you an array of Character (grapheme clusters) with O(1) integer subscripting. This copies the entire string, so it’s fine for short strings but avoid it in tight loops or with large text.
Which extension should I choose for a new project?
Use the StringProtocol extension (Fix #2) if you want zero-copy Substring results and standard-library-like behavior. Use the clamping String extension (Fix #3) if you want a forgiving API that never crashes and always returns String. Both are battle-tested; pick the one that matches your team’s safety vs. convenience preference.\n
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
