I have had a lot of discussions over the last decade on correct handling for out-of-memory conditions. There are two very distinct philosophical camps.
The Windows kernel has a policy of 'don't make promises you can't keep'. This means that a call to malloc will either return memory that is already available or the result of a new call to VirtualAlloc2, with committed memory. When the NT kernel says it has 'committed' memory, it means it promises that memory will be available when you try to access it. There is space reserved either in RAM or in swap to permit something else to be moved from RAM to make space for the thing that you need.
The other extreme says that you should assume that sometimes components fail. Erlang embodies this as the 'let it crash' philosophy. Google discovered fairly early on that, when you're looking at jobs distributed across large fleets of machines, a single node experiencing hardware failure is basically unavoidable. Mainframes deal with this by duplicating all of the hardware (often multiple times) so a hardware failure can be transparently worked around. But clouds built from consumer hardware don't do that: sometimes VMs die and you deal with it.
In the 'let it crash' model, you already need to build resilience at a higher level. Your individual jobs may fail because the hardware that runs on them fails. You need to be able to roll back any partially committed transactions that they started and restart them from the beginning. For things like web servers, that's usually easy: This HTTP request failed? The client can retry.
Once you have that mechanism, you rapidly realise that you can use it for other infrequent operations. Memory allocation almost never fails. If it does, let the program crash. Systems like iOS and macOS encourage programs to persist any user-visible state frequently, so a program crashing is fine - you may lose a couple of seconds work. In exchange for this, you get much simpler code. Rather than every allocation having to come with error handling (and there are tens of thousands of individual allocation points in a typical non-trivial program), you just assume it works and crash if it failed.
Some kernels, such as Linux and FreeBSD, lean into this even further. If the kernel claims it's given you memory, that isn't a promise, it's a best-effort guarantee. When you actually use it, they'll either ensure it's there or deliver a signal. It turns out most programs ask for memory that they'll need in their worst case, but this mechanism gives them the memory that they actually need. And this means that fewer allocations fail. You can't easily handle the allocation failure locally, but you're now less likely to hit allocation failure.
And this is a big win for overall reliability and performance. Because it turns out that gracefully handling out-of-memory conditions is really hard. Often, good error recovery requires allocating memory, but you can't do that if you've hit an out-of-memory condition. And these errors propagate outwards. Every function that allocates memory not may fail due to memory exhaustion. So it's not just routines that allocate memory that need to handle out-of-memory errors, it's everything that calls them. And you have to get it right at every place that needs to do any kind of cleanup if memory is exhausted.
This is one of the biggest reasons that embedded development is harder than application development: you can't assume memory exhaustion is rare and you don't always have an outer layer that can handle component-level failures. But in every other situation, the correct approach is to assume that memory is infinite and allocations never fail, and to assume that the component making that assumption will crash periodically.
This is a post about PHP.