If you have type safety, you can make the choice just once.
Rust's String::from_{utf8,utf16}_lossy turn valid UTF-8/16 sequences into strings, and "fix" invalid ones with U+FFFD
Meanwhile String::from_{utf8,utf16} attempt the same but with an Err instead of replacement on failure if that's what the programmer wants.
Imagine if all D's numeric functions took the same attitude as its string functions, insisting on being passed arrays of bytes so that each function can parse those bytes, decide if this is actually a 16-bit unsigned integer (for example) and if so do what's expected otherwise perhaps return an error. We'd spot right away that this was not a practical design.
D's choices here are conventional, but I've come to expect a lot more and so I'm disappointed when I can't have it.
#23405 was resolved as fixed a week ago. It isn't fixed. I guess at least I didn't waste my time filing the bug.
Having string be a magic builtin type does not eliminate the problem of dealing with invalid UTF sequences.
Invalid UTF sequences are inherent to the Unicode design, and programmers are left on their own to deal with it. The options are:
1. ignore them
2. use the replacement char
3. throw an exception (or other error indication)
D enables the programmer to pick which they need, on a case by case basis.