Can I make stack overflow warnings fatal?

I saw my Julia process writing Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable. to stderr after failed C-calls. The process is running as a systemd unit. Currently, I’m running a journalctl watcher that kills the Julia process on such rare occasions.

Is there a way to tell the Julia runtime to throw immediately instead of just writing a warning?

I think I’ve mitigated the root cause for the SO — I’m only asking about Julia exiting automatically.

I see “fixed on master” here, so this might apply (on second thought it likely doesn’t):

I’m looking into this, but need to run right now, what I found:

Thank you, I think the GitHub issue and the fix on master is about a type inference that may cause a stack overflow, not about the SO detection/warning.

The discourse topic you linked is interesting as it throws ERROR: LoadError: StackOverflowError:. I’ve never seen a StackOverflowError and a stacktrace on my workload, there was just the warning I mentioned above, a log message with a huge blob of C pointers, and the program kept running before crashing subsequently.

Obviously, there are some (many?) conditions that can trigger a SO and throwing would be a better fit for a long-running production process.

Stack overflows do throw! But it sounds like you’re catching it in a try block. You can make it fatal just by checking the catched exception.

julia> f() = g()
f (generic function with 1 method)

julia> g() = f()
g (generic function with 1 method)

julia> try
           g()
       catch
       end
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.

julia> try
           g()
       catch ex
           ex isa StackOverflowError && rethrow()
       end
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
ERROR: StackOverflowError:

That’s it! I have a call to Downloads.request(... throw = false) and a check for Downloads.RequestError only. The warnings I saw originated from the surrounding function. Rethrowing the other error types should fix it. see the next post below

The stack overflows happened rarely and in a live API only. That made it difficult to track them back to libcurl. Good catch, thank you very much!

I was wrong about rethrowing. The throw = false kwarg returns only a Downloads.RequestError. All other exceptions are thrown anyway.

The root cause seems to be more troublesome: in some circumstances, a try/catch block does not catch a StackOverflowError. The error is printed directly to stderr but the process continues running in a corrupted state.

EDIT: Indeed, asynchronous SOs cannot be caught reliably, see the GH issue What to do about asynchronous exceptions.


Excuse me for pasting a Claude generated blob; I wasn’t able to track this down by myself:

StackOverflowError skips try/catch/finally in compiled frames

I hit this chasing why a catch around some network code never fired on a stack overflow,
and the behaviour turned out to be more general than I expected: a StackOverflowError
does not run catch or finally in a compiled frame.
It unwinds past them to the
nearest interpreted handler — top level, typically — or, if it is inside a Task, it just
fails the task. No handler in between gets a chance to run.

MWE

f(n) = f(n + 1) + 1  # exhaust the stack

const ran = Ref(:neither)
g() = try f(0) catch; ran[] = :catch finally ran[] = :finally end

# 1. try/catch/finally in a compiled frame
esc = try g() catch e; typeof(e) end
println("1. compiled handler ran : ", ran[])
println("   escaped to top level : ", esc)

# 2. the same compiled handler, in a task
ran[] = :neither
t = @async g()
wait(t; throw = false)
println("2. compiled handler ran : ", ran[])
println("   istaskfailed         : ", istaskfailed(t), ", t.result::", typeof(t.result))

# 3. the same try/catch interpreted, in a task
body = :(try f(0) catch e; println("3. interpreted catch ran: ", typeof(e)) end)
t = @async Core.eval(Main, body)
wait(t; throw = false)
println("   istaskfailed         : ", istaskfailed(t))

Output (stdout):

1. compiled handler ran : neither
   escaped to top level : StackOverflowError
2. compiled handler ran : neither
   istaskfailed         : true, t.result::StackOverflowError
3. interpreted catch ran: StackOverflowError
   istaskfailed         : false

and on stderr, three raw writes from the runtime:

Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.

Case 1 is the surprise: g’s catch never runs, and neither does its finally, yet the
error is perfectly catchable one frame further out, at top level. Case 2 is the same thing
inside a task — the task fails, and the only in-process way to see it is at the join
(istaskfailed(t) / t.result). Case 3 is the control: exactly the same try/catch,
evaluated by the interpreter, catches it even inside a task.

The compiled-vs-interpreted reading is confirmed by --compile=min, where every handler in
the same script runs normally:

1. compiled handler ran : finally
   escaped to top level : catch
2. compiled handler ran : finally
   istaskfailed         : false, t.result::Symbol

Confirmed for Julia 1.13.0 on both macOS 27 and CentOS Stream 10.