Bug 276187 - Error.stack is missing the second frame in some cases
Summary: Error.stack is missing the second frame in some cases
Status: RESOLVED WONTFIX
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: Safari 17
Hardware: Unspecified macOS 13
: P2 Normal
Assignee: Nobody
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-07-03 12:10 PDT by krinklemail
Modified: 2024-07-04 15:53 PDT (History)
4 users (show)

See Also:


Attachments
Runtime Error.stack differs from Web Inspector showing the correct stack (467.37 KB, image/png)
2024-07-03 12:17 PDT, krinklemail
no flags Details
Caller context (368.80 KB, image/png)
2024-07-03 12:17 PDT, krinklemail
no flags Details
Caller-caller context (364.21 KB, image/png)
2024-07-03 12:17 PDT, krinklemail
no flags Details
Firefox comparison for reference (305.05 KB, image/png)
2024-07-03 12:18 PDT, krinklemail
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
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