Short answer: there is no “definitive way”.
How (and if it is possible) to patch (or prepend or append to) a command depends on how it's defined (\def, \newcommand, \DeclareRobustCommand, \NewDocumentCommand), if it takes arguments, what are the contents of the command, and what you want to patch in that command.
Macro without arguments: The simplest case is if the command doesn’t take arguments. In this case the macro is (in expl3 parlance) a “token list”, and you can just expand the macro and do whatever with its contents. Everything inside that token list is (unsurprisingly) a token, so you can traverse it, query its contents, add and remove tokens at your will. Prepending and appending in this case is trivial (see the definition of \g@addto@macro in latex.ltx), and the difficulty of patching something in the middle again depends on what you want to replace.
Macro with arguments: A trickier case is when the macro has arguments. In this case you cannot simply expand it because when you expand it tries to grab arguments, and you don’t have these here (and if you do, you lose the macro parameters inside the definition, which you probably don’t want).
In this case you have to use a proper patching command (like etoolbox’s \patchcmd). The process of patching consists in turning the entire definition into a string (using \meaning), separating the <parameter text> from the <replacement text>, doing the replacement, and then rescanning the string and redoing the definition using \scantokens, hoping nothing goes wrong in this process (see an explanation of that here).
The problem with patching is the \scantokens part, which assumes one single catcode regime, and depending how the macro was defined, that is not the case. Take for example LaTeX 2ε’s \rem@pt macro. If you do \meaning\rem@pt you get \rem@pt=macro:#1.#2pt->#1\ifnum #2>\z@ .#2\fi (suppose you just want to replace the . for a ,: seems easy enough). The problem with patching this one is that in #1.#2pt, the p and t are catcode-12 tokens, whereas the p and t in \rem@pt must be catcode-11 so that you can actually do the definition. \patchcmd will refuse to do anything here unless (it's not impossible, but you have to be careful). There are cases where this is outright impossible with \patchcmd (try patching anything into the definition of \end in a LaTeX older than 01-10-2019).
How it is defined (and optional arguments): Supposing that the macro has a well-behaved catcode regime, and that you can patch it using \patchcmd. When you define \newcommand\foo[1][]{a#1b} the actual definition of \foo is not in \foo, but in \\foo (two backslashes in the name), so you have to patch the latter instead. This case is quite easy: load xpatch and use \xpatchcmd (or \xpretocmd or \xapptocmd) instead. xpatch will do the thinking for you and will figure out that it needs to patch \\foo instead (and will use etoolbox’s \patchcmd for that). If the command was defined with xparse then xpatch won’t help either (these commands can be patched, but they are not meant to, so xpatch doesn’t even try to support that).
What you want to patch: You mentioned unbalanced \if...\fi in your question, but that’s actually not a problem. Unbalanced conditionals are problematic inside other conditionals, which is not the case when patching. The problem here is unbalanced {...}, since the patching macros assume balanced braces. Depending on what you want to do you can work-around (look here for an example), but it's really in a case-by-case basis.
l3regex to the rescue: What gets closest to a “definitive” way to patch is using l3regex (and regexpatch). The regexpatch package uses the LaTeX3 regular expression engine (l3regex) to match and replace tokens set under different catcode settings so that you can patch the trickier stuff. Using \regexpatchcmd\command{<search-regex>}{<replace-regex>}{<true code>}{<false code>} will look for the <search-regex> in the definition of the \command and will replace by the <replace-regex>. The package, same as xpatch, takes care of robust commands and commands with optional arguments. Take a look at the l3regex and regexpatch documentation for the syntax of the regular expressions and the patching commands.
\pretocmdto prepend and\apptocmdto append to a macro, and\patchcmdto patch some part of it. Though some times patching is tricker (example), and some times it's not at all possible. So again, it depends... – Phelype Oleinik Feb 21 '20 at 13:29\if\else. I updated the title to make it more clear. – Hyperplane Feb 21 '20 at 13:38xpatchloaded:\xpretocmd\-{\ifmmode \mycmd \else}{}{\ERROR}\xapptocmd\-{\fi}{}{\ERROR}. Patching with unbalanced\if...\fiis usually not a problem. What can be a complication is ubalanced{...}. – Phelype Oleinik Feb 21 '20 at 13:44xparsedefined commands, which is a bummer. – Hyperplane Feb 21 '20 at 13:51xpatchuses the patching mechanism frometoolbox, but adds extra checking in case the command is robust (\DeclareRobustCommand) or contains optional arguments. Andxparse-defined commands can be patched, yes (they are macros like any other), but they should not, that's why the author ofxpatchdidn't implement that. – Phelype Oleinik Feb 21 '20 at 13:53xparsecommands be patched? After all, they are macros like any other? – Mar 11 '20 at 07:39expl3tries to improve on that with a harder separation of internal code, so that it can be changed as long as functionality remains (try this for example). The same goes forxparsecode: as long as functionality remains, implementation can be freely changed, and whatever patch you have may stop working at any time. – Phelype Oleinik Mar 11 '20 at 11:35