How does a process end, and which signal or exit status does each kind of failure produce?
Predict first
Which outcome does each failure give? (the program re-runs itself with a mode argument)
import Foundation
if CommandLine.arguments.count > 1 {
let a = [1, 2, 3]
switch CommandLine.arguments[1] {
case "fatal": fatalError("boom")
case "unwrap": let x: Int? = Int("x"); print(x!)
case "index": print(a[Int(CommandLine.arguments[2])!])
case "segv": let p = UnsafeMutablePointer<Int>(bitPattern: Int("8")!)!; print(p.pointee)
case "abort": abort()
case "exit": exit(3)
case "term": kill(getpid(), SIGTERM)
case "kill": kill(getpid(), SIGKILL)
default: break
}
exit(0)
}
let names: [Int32: String] = [SIGTRAP: "SIGTRAP", SIGABRT: "SIGABRT", SIGSEGV: "SIGSEGV", SIGKILL: "SIGKILL", SIGTERM: "SIGTERM"]
func run(_ args: [String]) -> String {
let p = Process()
p.executableURL = URL(fileURLWithPath: CommandLine.arguments[0])
p.arguments = args
p.standardError = FileHandle.nullDevice
try! p.run(); p.waitUntilExit()
return p.terminationReason == .exit ? "exit status \(p.terminationStatus)" : "killed by \(names[p.terminationStatus] ?? "signal \(p.terminationStatus)")"
}
for args in [["exit"], ["fatal"], ["unwrap"], ["index", "7"], ["segv"], ["abort"], ["term"], ["kill"]] {
print(args.joined(separator: " ").padding(toLength: 8, withPad: " ", startingAt: 0), "->", run(args))
}
- exit 3 is a normal exit status; fatalError, force-unwrap and an out-of-range index are all SIGTRAP; a bad pointer is SIGSEGV; abort is SIGABRT; SIGTERM and SIGKILL come from the sender
- fatalError and force-unwrap give SIGABRT, the index gives SIGSEGV
- every failure is an exit status above 128
Mental model. A normal exit is an employee handing in their notice. A trap is an employee stopping work because an instruction made no sense. SIGKILL is security walking them out: no chance to clear the desk.
A process ends in one of two ways: it exits with a status (exit(3), or returning from main), or the kernel kills it with a signal. The check separates them with Process.terminationReason. Swift runtime failures (fatalError, a force-unwrap of nil, an array index out of range, integer overflow) execute a trap instruction and arrive as SIGTRAP on arm64 (reported in a crash log as EXC_BREAKPOINT); the same in an optimised build. A bad pointer dereference is SIGSEGV (or SIGBUS, EXC_BAD_ACCESS in the log), abort() (what an uncaught Objective-C exception ends in) is SIGABRT, and SIGTERM is the polite “please stop” that a process can catch. SIGKILL cannot be caught, blocked or observed: no handler, no applicationWillTerminate, no last log line. That is what the system uses for jetsam, for a watchdog timeout and for a code-signing violation, which is why those show up as a termination reason in a report rather than as a stack trace of your code.
In practice: a crash with a stack trace pointing at your code is a trap or a fault; a termination with an exception code like 0x8badf00d or a memory-limit reason (Q227, Q228) is the system killing you and the trace shows only where the main thread happened to be. Do not try to “catch” these in-process: installing a SIGSEGV handler to recover is undefined behaviour, and the only safe thing a handler may do is write a minimal report and exit. Crash reporters (and MetricKit diagnostics) record the signal and exception type for you, so read those fields first when triaging: they tell you whether to look for a bug in the code or for a resource problem. Exit statuses above 128 in a shell are 128 + signal number (133 is SIGTRAP, 137 is SIGKILL), which is how CI scripts report them.
Swift runtime checks trap (SIGTRAP), memory faults are SIGSEGV, and signals sent to the process are reported as such, not as exit statuses.
Common traps
- Expecting
applicationWillTerminateor a crash log for every death. A kill by the system gives your code no callback at all. - Treating
SIGTRAPas “a debugger breakpoint”. In Swift it is the normal signature of a failed precondition or force-unwrap.
They might follow up with
- Can an app handle
SIGKILL? No; design so that being killed at any instruction is safe (save progress early, make writes atomic). - Why does an uncaught Objective-C exception show as
SIGABRT? The runtime callsabort()after the exception handler finds no catch.
Related questions
- 6.6.2 What is jetsam, and how does it differ from a crash?
- 6.6.3 What is the watchdog, and what causes the 0x8badf00d exception?
- 6.9.2 Which memory number does the system judge your app by, and why do virtual size and “resident” mislead?
Which memory number does the system judge your app by, and why do… →