You make your life substantially easier if all the functions/methods/procedures/blocks/... in your code are "total" -- if they transform every possible input into the output one would expect from the name and type signature. With that as a guiding principle, I tend toward the following strategies, the particular choice of which depends on other details. Most often I'll combine 2/5, throwing a compile-time error for the broken branches, later realizing that the abstraction I chose was stupid, and then writing code that actually does what I want.

1. Inline the work being done. If you've only implemented a single case it doesn't deserve a named function that does something different from what the name indicates. Refactor it later if you ever need when you have more cases.

2. Ensure the error happens at compilation. C# has diagnostic directives. Zig has the @compileError builtin. C/C++ can fail using the preprocessor. It's a relatively common feature, and if you ensure that when your buggy/unfinished function compiles it's correct then that mitigates the harm you might cause (and serves as documentation: @compileError("TODO: ...")).

3. Ensure the type signature captures the problem. If some inputs result in crashes, return an error-code/error-condition/failure-union/optional/nullable/... so that the caller knows shit might happen and can deal with it at runtime if it does.

4. Make it abundantly clear that there's an issue. Name it `unsafe_unfinished_only_works_for_this_subtype_foo_bar` or something similarly onerous.

5. Use a different abstraction. Rather than `def handles_everything(something_that_might_not_be_handled):` -- a great abstraction that you don't actually use yet because you don't actually handle everything, instead write a `def handles_something(something):` function. Refactoring is cheap. If you don't need the full abstraction then don't use it, and definitely don't write an unfinished, buggy version of the abstraction you're not using.

5a. It might be hard to write the type signature of that restricted "something". E.g., let's say your function only works on sorted lists. A wrapper type pointing to the thing you actually want can be a good tool for this. E.g., you might have a SortedList type, and it might expose two methods for instantiating it -- validate_sorted (which might return an error but which checks that the list is actually in order), and unsafe_assert_sorted (where you as a programmer pinky promise that you know what you're doing and create the wrapper object regardless). Any function doing a binary search can now rely on the SortedList behaving appropriately, and all bugs associated with that function call can be tied to the very greppable name "unsafe_assert_sorted". Every possible input to the function yields an appropriate output. I'm not the first person to notice this idea. [0]

6. Limit the visibility. Use inner methods, inner classes, private, protected, underscore prefixes, or whatever else your language has to indicate to the outside world that they're technically allowed to use this method but that they probably want to think twice about it (similarly to the "unsafe_unfinished_..." from above, but also tends to play nicely with tooling).

7. Just finish the thing. Shaky foundations are more expensive than they appear, and when you've written one case it's actually usually pretty fast to implement the rest, especially while you have all the surrounding context in your mind.

8. Ignore all of the above. Sometimes something like performance overrides other concerns. Code is a balancing act.

[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...