Bug 276187

Summary: Error.stack is missing the second frame in some cases
Product: WebKit Reporter: krinklemail
Component: JavaScriptCoreAssignee: Nobody <webkit-unassigned>
Status: RESOLVED WONTFIX    
Severity: Normal CC: keith_miller, mark.lam, m.goleb+bugzilla, ysuzuki
Priority: P2    
Version: Safari 17   
Hardware: Unspecified   
OS: macOS 13   
Attachments:
Description Flags
Runtime Error.stack differs from Web Inspector showing the correct stack
none
Caller context
none
Caller-caller context
none
Firefox comparison for reference none

Description krinklemail 2024-07-03 12:10:35 PDT
When creating `new Error` and reading `e.stack` there is consistently (in certain situations) a second or third frame missing.

Test case:
https://codepen.io/Krinkle/pen/jOoggZw

In Firefox, Chrome, the raw stack is:

```
[0] sourceFromStacktrace
[1] QUnit.stack
[2] exampleBar
[3] exampleFoo
[4] exampleMain
[5] makeFakeFailure
```

In Safari, one of the intermediary frame has excluded.

```
[0] sourceFromStacktrace
[1] exampleBar
[2] exampleFoo
[3] exampleMain
[4] makeFakeFailure
```

This is problematic because in diagnostic tools and test frameworks like QUnit, a function like QUnit.stack() will often omit the first few frames for developer convenience, hiding its known internal offset. In the case of the above, QUnit would strip the first 2 frames when QUnit.stack is directly directly, or possibly more when passed an offset for additional wrapper functions.

The practical impact is that slice(2) ends up removing part of the user's own stack. Thus misleading them into thinking there is an error thrown by exampleFoo, when actually it caused deeper down in exampleBar.

I suspect this may be some kind of JIT operation combined with a failure to retain/restore the stack trace. Or perhaps a privacy hueristic misfiring. However, I've been unable to reproduce with an inline script tag, only with an external script. The above CodePen copies the two functions from qunit.js ("live") and embeds them directly ("reduced"). The issue does not happen in that case.

Safari Version 17.5 (18618.2.12.111.5, 18618)
Comment 1 krinklemail 2024-07-03 12:13:43 PDT
Keywords: stacktrace, stack trace, callstack, call stack, exception.

Possibly related: https://bugs.webkit.org/show_bug.cgi?id=70605
Comment 2 krinklemail 2024-07-03 12:17:26 PDT
Created attachment 471806 [details]
Runtime Error.stack differs from Web Inspector showing the correct stack
Comment 3 krinklemail 2024-07-03 12:17:39 PDT
Created attachment 471807 [details]
Caller context
Comment 4 krinklemail 2024-07-03 12:17:51 PDT
Created attachment 471808 [details]
Caller-caller context
Comment 5 krinklemail 2024-07-03 12:18:05 PDT
Created attachment 471809 [details]
Firefox comparison for reference
Comment 6 Yusuke Suzuki 2024-07-04 11:26:21 PDT
This is the correct behavior because ECMAScript spec defines tail-calls, and QUnit.stack is taking tail-call form (strict mode, and calling sourceFromStacktrace in tail-call position).
So, Firefox and Chrome behaviors are wrong in terms of the spec.
Comment 7 Yusuke Suzuki 2024-07-04 11:28:41 PDT
You can ask to Firefox / Chrome to implement tail-call as spec requires, and then QUnit implementation should be fixed.
Comment 8 krinklemail 2024-07-04 11:54:55 PDT
Thanks, I was able to finally reproduce this in isolation by changing:


```
var QUnit_reduced = {
	stack: function (offset) {
		offset = (offset || 0) + 2;
		return sourceFromStacktrace(offset);
	}
}
```

to

```
var QUnit_reduced = {
	stack: function (offset) {
		'use strict';
		offset = (offset || 0) + 2;
		return sourceFromStacktrace(offset);
	}
}
```

Related reading:
* https://262.ecma-international.org/11.0/#sec-function-calls-runtime-semantics-evaluation - Defining IsInTailPosition
* https://262.ecma-international.org/11.0/#sec-evaluatecall - tailCall/tailPosition dictating "as if it has already returned"
* https://stackoverflow.com/a/54721813/319266 - Tail Calls introduced in ES6 and shipped by Safari, but defacto JSC/WebKit-only at this point.
* https://github.com/tc39/proposal-error-stacks/ - seeking to standardise Error.stack
* https://github.com/tc39/proposal-error-stacks/issues/23