In C programming, stack overflow errors are outside of the language spec. They represent a fundamental violation of the "contract" of what a function call means. You can overflow the stack halfway through pushing arguments to a function. Or you can overflow it mid-way through a library routine like malloc()
(internally to its own implementation might make several function calls that grow the stack, any one of which could overflow). An overflow could happen halfway through the bookkeeping it is doing for the allocation...leaving the heap in a corrupted state that would crash the next malloc()
or free()
if you tried to keep running.
In theory, it seems JavaScript could do better. It is not running on bare metal...so it could offer some kind of guarantees about operations that would not overflow the stack in mid-run (e.g. by preallocating memory for usermode recursions, vs. making every JS call trigger some C-level recursion in the interpreter). A JS interpreter could give you enough atomic building blocks for making a kind of transaction...where all of a set of operations would run (with no stack overflow) or none of them would (pre-emptively triggering a stack overflow). These transactions could be a wedge for monitoring what you would need to roll back at the moment of catching a RangeError exception upon running out of stack.
In practice, it seems to me that you're not much better off than C. So I think this answer is correct (2 upvotes at time of writing):
...and I think this answer is--at minimum--misleading in a generic sense (6 upvotes at time of writing):
"Maximum call stack size exceeded errors can be caught just like any other errors"
As an example, imagine I have this routine, and I wish to maintain the invariant that you never end up in a situation where the two named collections don't each have a value for a given key:
function addToBothCollections(key, n) {
collection1[key] = n
n = n + Math.random() // what if Math.random() overflows?
collection2[key] = n
}
If I had a "wedge" of guaranteed operations that would not overflow the stack, I could come up with a protocol where those operations were used to build some kind of transaction log. Then if an overflow occurred, I could tailor operations like this addToBothCollections() to take advantage of it.
e.g. imagine that negative numbers aren't allowed, so I could say:
function addToBothCollections(key, n) {
collection1[key] = n
collection2[key] = -1
n = n + Math.random()
collection2[key] = n
}
try {
/* ...code that makes a bunch of addToBothCollections() calls */
}
catch (e) {
for (let key in collection2) {
if (collection2[key] == -1) {
delete collection1[key]
delete collection2[key]
}
}
}
But thinking along these lines can only work if you have guarantees, such as that this sequence will either atomically execute both operations or neither:
collection1[key] = n
collection2[key] = -1
If there's any way that collection2[key] = -1
might do something like trigger a GC that causes a stack overflow, you're back to square one. Some atomicity rules are needed.
So is there anything at a language level--current or future--that articulates this kind of problem? I'm actually curious about how this would apply to mixed JavaScript and WebAssembly programs...so if there's any way that you could cook up the atomicity I speak of in Wasm I'd be fine with that as well.