denoland / deno

A modern runtime for JavaScript and TypeScript.
https://deno.com
MIT License
98.02k stars 5.39k forks source link

Stack traces are not printed when modified by the user #21206

Open jespertheend opened 1 year ago

jespertheend commented 1 year ago
const e = new Error("oh no");
e.stack = "oh no\nSome very useful information about the origin of the error";

throw e;

When you run this from the browser console, all browsers except Firefox display the stack trace:

image

But Deno actually removes the existing stack trace completely and only logs the error message:

image
nayeemrmn commented 1 year ago

Uncaught error formatting doesn't use error.stack but the structured information error.stack is made from. I suggest modifying error.message instead or using error.cause.

With that said, it should leave the 'true' stack trace untouched rather than deleting it. Seems that happens when error.stack is set by the user before it's ever accessed, so Error.prepareStackTrace() (our way of extracting the structured stack trace) isn't called. For example it wouldn't happen if you had e.stack; between the first two lines. Not sure how to fix that.

jespertheend commented 1 year ago

Hmm, well my use case is that errors are serialized and sent over the network, and then thrown on another client with the modified stacktrace. Leaving the full stack trace info in error.message feels wrong in that case.

andykais commented 7 months ago

I have a similar use case to @jespertheend. I attempted to use Error.prepareStackTrace which fails to do what I expect here. I outlined an example below:

class RemoteError extends Error {
  constructor(message: string, server_callstack: string) {
    const server_error_client_representation = new ServerErrorClientRepresentation(message, server_callstack)
    super(`Remote error: ${message}`, { cause: server_error_client_representation })
  }
}

class ServerErrorClientRepresentation extends Error {
  constructor(message: string, server_callstack: string) {
    super(message)

    const prepare_stack_trace = Error.prepareStackTrace
    Error.prepareStackTrace = () => {
      console.log('prepareStackTrace called')
      return server_callstack
    }
    const stack = this.stack
    Error.prepareStackTrace = prepare_stack_trace
  }
}

function server_code_foobar() {
  throw new Error('foobar')
}
function server_code() {
  try {
    const result = server_code_foobar()
    return JSON.stringify({ result })
  } catch (e) {
    return JSON.stringify({ error: {message: e.message, stack: e.stack} })
  }
}

function client_code() {
  // this happens over the wire in reality
  const remote_result = JSON.parse(server_code())
  if (remote_result.error) {
    throw new RemoteError(remote_result.error.message, remote_result.error.stack)
  } else {
    return remote_result
  }

}

try {
  client_code()
} catch(client_side_error) {
  console.log(client_side_error)
  console.log()
  console.log('---==== Deno Error Print ====---')
  throw client_side_error
}

this snippet outputs the following:

prepareStackTrace called
Error: Remote error: foobar
    at client_code (file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:39:11)
    at file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:47:3
Caused by Error: foobar
    at server_code_foobar (file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:24:9)
    at server_code (file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:28:20)
    at client_code (file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:37:36)
    at file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:47:3

---==== Deno Error Print ====---
error: Uncaught (in promise) Error: Remote error: foobar
    throw new RemoteError(remote_result.error.message, remote_result.error.stack)
          ^
    at client_code (file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:39:11)
    at file:///home/andrew/Code/ts-rpc/scratchwork/prepare_stacktrace_repro.ts:47:3
Caused by: Error: foobar

it looks like console.log correctly prints the modified stacktrace, but deno's native exception (which in my case is important for E2E client/server tests) will ignore any custom stacktrace.