String#insert
str.insert(index, other_str) String#insert inserts the given string at a given index. It returns a modified copy, leaving the original string untouched.
Unlike most string methods that take a single index, insert accepts negative indices — counting from the end of the string, similar to Array#insert.
Signature
str.insert(index, other_str)
Parameters:
index— an Integer indicating the position (0-based, or negative to count from end)other_str— the String to insert
Returns: String — a new string with other_str inserted at index
Raises IndexError if index is out of range, and TypeError if other_str is not a string.
Basic Usage
"hello".insert(1, " X") # => "hXello"
"hello".insert(5, "!") # => "hello!"
"hello".insert(-1, "!") # => "hello!"
The positive-index examples show the straightforward case: pick a position counting from the left and the new string slides in at that point. No characters are overwritten — everything from the insertion point onward shifts to the right by the length of the inserted string. This non-destructive-in-place behavior is what makes insert distinct from []= (element assignment), which replaces characters instead of shifting them.
Positive indices insert at that character position. The character at that position is not replaced — everything from there onward shifts to the right.
Negative indices
This is where insert differs from most string methods:
"hello".insert(-2, "X") # => "helXlo"
"hello".insert(-5, "X") # => "Xhello"
"hello".insert(-10, "X") # => IndexError
Negative indices let you insert relative to the end of the string without knowing its length in advance. -1 targets the position just before the final character, -2 the spot before the second-to-last character, and so on. An index that would place the insertion point before the start of the string — such as -10 on a five-character string — raises an IndexError rather than silently clamping.
-1 is the last character, -2 is second-to-last, and so on. An index that would place before the start of the string raises an IndexError.
Prepending and appending
Use index 0 to insert at the beginning:
"world".insert(0, "hello ") # => "hello world"
Prepending with insert(0, ...) puts the new content before the first character of the original string, effectively adding a prefix. Appending is trickier — insert(-1, ...) places content before the last character, not after it. To add a true suffix, use insert(length, ...) which targets the position just past the final character, or reach for concat/<< which are purpose-built for appending.
Use index -1 to insert before the last character — effectively appending if you follow it:
"hello".insert(-1, " world") # => "hello world"
Both insert(0, ...) and insert(-1, ...) cover the common prefix and suffix operations, but each has a subtle edge. Inserting at 0 puts the new content before the first character, while inserting at -1 puts it before the last character. If you need to append after the final character, use insert(length, ...) or reach for concat/<<, which are purpose-built for appending and communicate intent more clearly.
Or more directly, use + str or concat to append without the index gymnastics.
Edge cases
IndexError for out-of-range
"hello".insert(100, "!") # => IndexError: index 100 too big for string of length 5
"hello".insert(-100, "!") # => IndexError: index -100 too small for string of length 5
The IndexError examples above cover both directions: an index beyond the string length on the right side, and an index before the string start on the left side. The permitted range is exactly -length through length, and any value outside this band raises an error immediately. This strict boundary checking means you do not need to handle silent truncation or clamping — the method simply refuses to operate on an invalid position, which makes debugging easier when an index computation goes wrong.
TypeError for non-string argument
"hello".insert(0, 123) # => TypeError: no implicit conversion of Integer into String
The second argument must be a string — Ruby does not perform implicit type conversion on the inserted value. Passing an integer, float, or any other non-string object raises a TypeError with a message like “no implicit conversion of Integer into String.” The fix is simple: call .to_s on the value first, or use string interpolation to build the final string in a single expression rather than relying on insert to handle type coercion.
The second argument must be a string. Numeric values need explicit conversion:
"hello".insert(0, 123.to_s) # => "123hello"
The to_s conversion above is the standard workaround when you have a numeric value and need to insert it into a string at a specific position. A more idiomatic alternative is to use string interpolation — "hello".insert(0, "#{123}") — which achieves the same result without an explicit conversion call. Both approaches produce identical output, so choose whichever reads more clearly in context.
Modifying a string in place
insert returns a new string — the original is unchanged. For in-place modification:
s = "hello"
s = s.insert(0, "j") # rebind to the new string
puts s # => "jhello"
There is no insert! variant.
The lack of a bang-variant insert! is worth noting because it means every call to insert produces a new string object. In a loop or a performance-sensitive path where you are building up a long string through repeated insertions, this allocation cost can add up. For those cases, consider building the string with an array and join, or using a mutable StringIO buffer, rather than calling insert on an ever-growing string.
Use cases
Inserting a Separator
"20260430".insert(4, "-").insert(7, "-") # => "2026-04-30"
Chaining two insert calls transforms a raw date string into a formatted one with hyphens at positions 4 and 7. The second call must account for the shift introduced by the first insertion — after inserting at position 4, the string is one character longer, so the second hyphen goes at position 7 rather than 6. This chaining pattern works when you know exact offsets and the insertions do not overlap, but it becomes fragile quickly as the number of insertions grows.
Chaining multiple insert calls can format strings where you know the exact positions.
Adding delimiters in a loop
numbers = ["a", "b", "c"]
result = ""
numbers.each_with_index do |n, i|
result = result.insert(i * 2, n)
end
result # => "abc"
The loop-based approach demonstrates why insert is not the right tool for building strings incrementally. Each call allocates a new string, and the index calculation becomes harder to reason about as the string grows. For concatenation with separators, Ruby’s join method is both clearer and more efficient — it builds the final string in a single allocation rather than through repeated insertions.
This gets awkward fast — join is usually a better choice for concatenation with separators.
Dynamic text insertion
def label(type, version)
"App #{type}".insert(-1, " v#{version}")
end
label("Beta", 2) # => "App Beta v2"
insert vs other methods
| Method | Returns new? | Accepts negative index? |
|---|---|---|
insert | Yes | Yes |
[]= | No (in-place) | Yes |
concat | Yes | No |
prepend | Yes | No |
insert is the only string mutator that combines non-destructive behavior with negative indexing.
See Also
- /reference/string-methods/string-prepend/ — prepend a string to the beginning
- /reference/string-methods/concat/ — concatenate one or more strings
- /reference/hash-methods/slice/ — extract or replace substrings by index