Skip to content

errors: make Exception.message/1 plain; add Lua.format_exception/1#393

Merged
davydog187 merged 2 commits into
mainfrom
errors-plain-message-rich-format
Jul 15, 2026
Merged

errors: make Exception.message/1 plain; add Lua.format_exception/1#393
davydog187 merged 2 commits into
mainfrom
errors-plain-message-rich-format

Conversation

@davydog187

Copy link
Copy Markdown
Contributor

Context

A user reported that Exception.message/1 on Lua's exceptions embeds ANSI color and a multi-line layout (location header, source context, stack trace, suggestion). When they start their app in dev from a terminal, IO.ANSI.enabled?/0 is legitimately true, so every Logger.error(Exception.message(e)) gets colored, multi-line log lines.

The root cause is a layering issue: IO.ANSI.enabled?/0 answers "is stdout a TTY?", a poor proxy for "is this string about to be painted onto an interactive terminal?". The two come apart the moment the string goes to Logger instead of IO.puts. And ANSI is only the visible symptom — even with color off, the message was still multi-line.

Colored, rich rendering is only genuinely useful for a human reading an error interactively — i.e. mix lua.eval and uncaught crashes. So this PR moves it there and makes the library default plain.

Changes

  • Exception.message/1 (on Lua.RuntimeException / Lua.CompilerException) now returns a plain, single-line, ANSI-free string — the error body plus a compact (at source:line) suffix — safe for Logger and error trackers.
  • Lua.format_exception/1 renders the rich, ANSI-gated report (location, stack trace, suggestions) and is what mix lua.eval now prints. Each internal VM error gained a format/1 sibling to its message/1 / to_map/2.
  • Lua.RuntimeException.to_map/2 and Lua.CompilerException.to_map/1 expose the existing wire-safe structured shape (no ANSI) for JSON payloads, structured logs, and UI-facing reporting.

Before / after (app started from a TTY, ansi_enabled: true)

# before: colored + multi-line in the log
Logger.error(Exception.message(e))

# after: one clean line, no escapes
Exception.message(e)
#=> "Lua runtime error: attempt to perform arithmetic on a nil value (local 'x') (at demo.lua:2)"

Lua.format_exception(e)   # rich report, colored on a TTY — for mix lua.eval / interactive use
Lua.RuntimeException.to_map(e)   # %{message:, source:, line:, call_stack:, ...} — for UIs

Safety: Lua §6.1 is untouched

The value pcall/xpcall/Lua.call_function hand back to Lua goes through Lua.VM.ProtectedCall.error_value/1 (raw :value / :lua_value / ArgumentError.raw_message), not Exception.message/1. Verified: pcall(error('boom')) still returns {false, "<eval>:1: boom"}.

Compatibility note

If you relied on Exception.message/1 for the rich render, switch to Lua.format_exception/1. Message strings aren't a stable API, but this is called out in the CHANGELOG since the output format visibly changes.

Verification

  • mix test2611 passed, 7 skipped; mix format --check-formatted clean; compiles with --warnings-as-errors.
  • mix lua.eval on a failing script (piped/non-TTY): full rich report, zero ANSI escapes.
  • Kevin's scenario (ansi_enabled: true): Exception.message/1 has no \e[ and no \n.

`Exception.message/1` on `Lua.RuntimeException` and `Lua.CompilerException`
embedded ANSI color and a multi-line layout (location header, source
context, stack trace, suggestion). ANSI was gated on `IO.ANSI.enabled?/0`,
but that gate answers "is stdout a TTY?" — a poor proxy for "is this string
about to be painted onto an interactive terminal?". A host that starts its
app from a terminal and logs `Exception.message(e)` gets colored, multi-line
log lines even though the destination is `Logger`, not the TTY.

Colored, rich rendering is only useful for a human reading an error
interactively — i.e. `mix lua.eval` and uncaught crashes. So move it there:

- `Exception.message/1` now returns a plain, single-line, ANSI-free string
  (error body + a compact `(at source:line)` suffix), safe for `Logger` and
  error trackers.
- `Lua.format_exception/1` renders the rich, ANSI-gated report and is what
  `mix lua.eval` prints. Each internal VM error gained a `format/1` sibling
  to `message/1`/`to_map/2`.
- `Lua.RuntimeException.to_map/2` and `Lua.CompilerException.to_map/1` expose
  the existing wire-safe structured shape for JSON / UI reporting.

Lua §6.1 semantics are untouched: pcall/xpcall/call_function hand back the
raw value via `ProtectedCall.error_value/1`, never `Exception.message/1`.
Comment thread CHANGELOG.md Outdated
The Exception.message/1 / Lua.format_exception/1 split ships as part of
1.0.0, not a later release, so move its entries out of [Unreleased] and
into the 1.0.0 section. Reconcile the #384 entries that described the
intermediate behavior (#393 supersedes: message is plain, rich report
moves to Lua.format_exception/1) so the release reads coherently.
@davydog187 davydog187 merged commit 69e13a6 into main Jul 15, 2026
5 checks passed
@davydog187 davydog187 deleted the errors-plain-message-rich-format branch July 15, 2026 21:37
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