One benefit of using the conventional "bootloader" approach (which is a piece of code that gets executed first thing upon reset) is that, unless the bootloader itself is buggy (but you can make it as simple and robust as possible), it makes the device virtually unbrickable, as there will always be a way for the user of reflashing the firmware if something goes wrong.
If the flashing code is part of your firmware, it's more prone to making the device brickable if a firmware update fails.
True only if the "usual" update route is through some very simple physical interface, like serial port, and your customers are fine doing the normal updates through this route. But this is rarely the case today. If you want over-the-air update, maybe even secure, then it has all the complexity of the application, and the related risks, and it's actually
safer to have that as part of the app. If you then fuck up the app so it can't connect and update, then the device is "bricked" to the state of needing a physical cable - that would just be SWD/JTAG/etc. instead of say UART.
Today normal people won't have either simple serial port cables or SWD cables hanging around, so really you need to service the bricked devices, or supply your customer with an "update cable", no way around it. So I prefer the way of having a bootloader which does nothing else but checks CRC and boots, and then use the application to update the application. Otherwise, over-the-air updates would be impossible, or I would need updateable bootloaders which is then again a total mess.
I manage the risk by careful testing that whenever I release the new firmware, make damn sure it boots and accepts new firmware through the usual route; test that well, even if you must half-ass testing everything else.
(In some cases, I have had to work with devices which have either only one flash sector (eraseable unit) or too little flash to hold two applications. In such cases, update process always has power loss brickage risk. But it's the simplest case to design for; just make sure everything related to the update is fully in RAM, erase and write; job done. But when doing this stuff properly, you would always erase and write a different part in flash, and then you don't have to put the flasher code in RAM; just execute from flash as usual, because you are not going to erase that half.)