Skip to content

An executable and the JIT say what an exception nothing caught threw (Windows and Linux) - #833

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-uncaught-exception-exe
Nov 12, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-uncaught-exception-exe

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

An uncaught exception used to end a program without saying what was thrown.

  • Windows: the C runtime's terminate() called abort(), which exited silently with 3, or 0xC0000409 with --opt.
  • Linux: libstdc++ printed terminate called after throwing an instance of 'char const*' and the program died of SIGABRT. Under the JIT, LLVM's crash handler reported it as a crash of the compiler.

Either way the program's earlier output, still in stdout's buffer, was lost. That is why JSON.stringify({x: 1}) looked like a crash (fixed in #832). The Windows JIT already reported error: uncaught exception: <what>.

function main() { print("before"); throw new TypeError("boom"); }

Now, as an executable or under the JIT, on Windows and Linux:

  • before on stdout
  • error: uncaught exception: TypeError: boom on stderr (the JIT adds its tslang: prefix)
  • exit code 1

Change

  • TypeScriptAsyncRuntime/UncaughtException.cpp (new): its initializer sets the handler.

    • Windows: an unhandled-exception filter in front of the C runtime's, which acts on C++ throws only.
    • Linux: a std::terminate handler. terminate() called with no exception (e.g. a joinable std::thread destroyed) goes to the previous handler, as before.
    • On an uncaught throw, the handler flushes stdout, describes the value on stderr and exits with 1 (TerminateProcess / _exit), as the JIT does.
  • tslang/exe.cpp: every executable is linked with -include:__tslang_report_uncaught (Windows) or -Wl,-u,__tslang_report_uncaught (Linux), placed before the runtime archive. The archive is checked for the symbol first, so a runtime older than this links as before. Android is unchanged.

  • tslang/jit.cpp: on Linux the JIT sets the same terminate handler around the entry call.

  • TypeScript/UncaughtException.h (new): the code that describes the value, which both use. It describes:

    • a string
    • a number, as the program prints it
    • a bool, an int or a bigint
    • a class, by its name
    • an Error, with its message

    On Windows it reads the exception record, as jit.cpp did; that code is moved here unchanged. On Linux it reads the current exception through std::current_exception() and its type_info name (PKc, d, P7Failure, …). An Error is found by walking the class type_info's base chain. tslang is built without RTTI, so the Itanium layouts are read directly.

Tests

  • uncaught-exe.cmake builds the four uncaught/*.ts programs with --emit=exe, under gc, rc, none and own, with and without --opt: 32 tests. Each checks exit code 1, before on stdout and the description on stderr.
  • These, and the existing JIT uncaught tests, now also run on Linux.

Verified locally

  • Windows x64:
    • ctest: 7024/7024 passed.
    • DefaultLib tests.ps1 passes for all four models, release and debug, compiled and JIT. This run predates the Linux commit, which leaves the Windows code as it was.
  • Linux (WSL, Ubuntu 26.04):
    • The 44 uncaught tests pass.
    • With the DefaultLib, throw new TypeError("boom") prints error: uncaught exception: TypeError: boom.
    • Full ctest: 7004/7005. The one failure is test-compile-shared-import-chain-rtti-skipped, a type-checking error ("Class member 'reports' can't be found") in a test unrelated to this change. It passed in this PR's earlier Linux CI run, which had the same compiler front end, and looks specific to the reused local WSL build tree. Linux CI will confirm.

Not covered

  • Android keeps libc++'s default handler.
  • Programs that test-runner links itself. test-runner links its own object files, so they do not take the runtime file. Only tslang --emit=exe does.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits November 12, 2026 22:40
An uncaught throw in a Windows executable reached the C runtime's terminate(), whose abort()
ended the program with exit code 3 (0xC0000409 with --opt) and not a word - not even what it
had printed, still in stdout's buffer. That is how JSON.stringify's TypeError for an object
literal looked like a crash. The JIT already said "error: uncaught exception: <what>".

Now an executable says the same: TypeScriptAsyncRuntime/UncaughtException.cpp puts an
unhandled-exception filter in front of the C runtime's, which flushes stdout, writes the
description to stderr and ends the program with exit code 1. tslang links it into every
executable with -include:__tslang_report_uncaught, when the runtime has it (an older runtime
links as before). The description (a string, a number as the program prints it, a class by its
name, an Error with its message) moved from jit.cpp to TypeScript/UncaughtException.h, which
both use.

Tests: uncaught-exe.cmake, the four uncaught/*.ts programs under gc, rc, none and own, with
and without --opt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… threw

On Linux an uncaught throw went to libstdc++'s terminate handler ("terminate called after
throwing an instance of 'char const*'") and SIGABRT, and the program's buffered output was
lost; under the JIT LLVM's crash handler reported it. Now the runtime's UncaughtException.cpp
sets a std::terminate handler there, linked into an executable with `-u __tslang_report_uncaught`
(when the runtime has it), and the JIT sets the same around the entry call: stdout flushed,
"error: uncaught exception: <what>" on stderr, exit code 1.

describeCurrentException (TypeScript/UncaughtException.h) reads the current exception from
std::current_exception() and its type_info's name (`PKc`, `d`, `P7Failure`, ...); an Error's
message is found through the class type_info's base chain. tslang is built without RTTI, so the
Itanium layouts are read as they are.

The uncaught tests (JIT and executable) now run on Linux too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77 ASDAlexander77 changed the title An executable says what an exception nothing caught threw An executable and the JIT say what an exception nothing caught threw (Windows and Linux) Nov 12, 2026
@ASDAlexander77
ASDAlexander77 merged commit a591af4 into main Nov 12, 2026
8 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-uncaught-exception-exe branch November 12, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant