Generators were introduced in EcmaScript 2015. They provide a way to iterate over a set of values that are produced by a generating function.
Here's an example of how to use a generator:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
console.log(aGenerator.next()); // outputs { value: 2, done: false }
console.log(aGenerator.next()); // outputs { value: 3, done: false }
console.log(aGenerator.next()); // outputs { value: undefined, done: true }
Generators are stateful, and in order to be in a coherent state even in the face of uncaught exceptions, they need to catch them in two different situations:
The consequences is that the following sample code:
function boom() {
throw new Error('boom');
}
function bar() {
boom();
}
function foo() {
bar();
}
function* generatorThatThrows() {
yield foo();
}
var generator = generatorThatThrows();
console.log(generator.next());
when run with the --abort-on-uncaught-exception
command line switch:
$ node --abort-on-uncaught-exception generator-that-throws.js
will generate a core file that does not have the functions foo
, bar
or
boom
active on the main thread's call stack:
[root@dev /home/jgilli]# node --abort-on-uncaught-exception generator-that-throws.js
Uncaught Error: boom
FROM
Object.<anonymous> (/home/jgilli/generator-that-throws:18:23)
Module._compile (module.js:410:26)
Object.Module._extensions..js (module.js:417:10)
Module.load (module.js:344:32)
Function.Module._load (module.js:301:12)
Function.Module.runMain (module.js:442:10)
startup (node.js:136:18)
node.js:966:3
Illegal Instruction (core dumped)
[root@dev /home/jgilli]# ls cores/
core.node.10771
[root@dev /home/jgilli]# mdb cores/core.node.10771
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
(1 internal frame elided)
js: next
(1 internal frame elided)
js: <anonymous> (as <anon>)
(1 internal frame elided)
js: <anonymous> (as Module._compile)
js: <anonymous> (as Module._extensions..js)
js: <anonymous> (as Module.load)
js: <anonymous> (as Module._load)
js: <anonymous> (as Module.runMain)
js: startup
js: <anonymous> (as <anon>)
(1 internal frame elided)
(1 internal frame elided)
native: v8::internal::Execution::Call+0x14f
native: v8::Function::Call+0xff
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x508
native: _start+0x6c
>
When --abort-on-uncaught-exception
is used, if an error is thrown from
within a generator and is not caught, there is no point in resetting the state
of the generator for the following reasons:
-
The process is going to abort anyway, so the generator won't be used to run any further code.
-
When inspecting the resulting core dump, preserving the generator in the state when the uncaught error was thrown is actually desirable.
Here's a patch that implements this approach:
diff --git a/deps/v8/src/js/generator.js b/deps/v8/src/js/generator.js
index 2f61b3f..2f2f39d 100644
--- a/deps/v8/src/js/generator.js
+++ b/deps/v8/src/js/generator.js
@@ -38,11 +38,15 @@ function GeneratorObjectNext(value) {
if (continuation > 0) {
// Generator is suspended.
DEBUG_PREPARE_STEP_IN_IF_STEPPING(this);
- try {
+ if (%IsAbortOnUncaughtException()) {
return %_GeneratorNext(this, value);
- } catch (e) {
- %GeneratorClose(this);
- throw e;
+ } else {
+ try {
+ return %_GeneratorNext(this, value);
+ } catch (e) {
+ %GeneratorClose(this);
+ throw e;
+ }
}
} else if (continuation == 0) {
// Generator is already closed.
@@ -63,11 +67,15 @@ function GeneratorObjectThrow(exn) {
var continuation = %GeneratorGetContinuation(this);
if (continuation > 0) {
// Generator is suspended.
- try {
+ if (%IsAbortOnUncaughtException()) {
return %_GeneratorThrow(this, exn);
- } catch (e) {
- %GeneratorClose(this);
- throw e;
+ } else {
+ try {
+ return %_GeneratorThrow(this, exn);
+ } catch (e) {
+ %GeneratorClose(this);
+ throw e;
+ }
}
} else if (continuation == 0) {
// Generator is already closed.
diff --git a/deps/v8/src/runtime/runtime-internal.cc b/deps/v8/src/runtime/runtime-internal.cc
index 478a954..874dad1 100644
--- a/deps/v8/src/runtime/runtime-internal.cc
+++ b/deps/v8/src/runtime/runtime-internal.cc
@@ -187,6 +187,15 @@ RUNTIME_FUNCTION(Runtime_PromiseRejectEvent) {
return isolate->heap()->undefined_value();
}
+RUNTIME_FUNCTION(Runtime_IsAbortOnUncaughtException) {
+ HandleScope scope(isolate);
+ if (FLAG_abort_on_uncaught_exception) {
+ return isolate->heap()->true_value();
+ }
+
+ return isolate->heap()->false_value();
+}
RUNTIME_FUNCTION(Runtime_PromiseRevokeReject) {
DCHECK(args.length() == 1);
diff --git a/deps/v8/src/runtime/runtime.h b/deps/v8/src/runtime/runtime.h
index 23f9bdc..467d42c 100644
--- a/deps/v8/src/runtime/runtime.h
+++ b/deps/v8/src/runtime/runtime.h
@@ -311,6 +311,7 @@ namespace internal {
#define FOR_EACH_INTRINSIC_INTERNAL(F) \
+ F(IsAbortOnUncaughtException, 0, 1) \
F(CheckIsBootstrapping, 0, 1) \
F(ExportFromRuntime, 1, 1) \
F(ExportExperimentalFromRuntime, 1, 1) \
Building node's master with this patch and running the same sample code gives the following output:
jgilli@dev ~/node]$ ./node --abort-on-uncaught-exception generator-that-throws.js
Uncaught Error: boom
FROM
boom (/home/jgilli/generators-js/generator-that-throws.js:2:5)
bar (/home/jgilli/generators-js/generator-that-throws.js:6:5)
foo (/home/jgilli/generators-js/generator-that-throws.js:10:5)
generatorThatThrows (/home/jgilli/generators-js/generator-that-throws.js:14:11)
Object.<anonymous> (/home/jgilli/generators-js/generator-that-throws.js:18:23)
Module._compile (module.js:417:34)
Object.Module._extensions..js (module.js:426:10)
Module.load (module.js:357:32)
Function.Module._load (module.js:314:12)
Function.Module.runMain (module.js:451:10)
startup (node.js:140:18)
node.js:1004:3
Illegal Instruction (core dumped)
And we can see that the core file was generated at the time the initial
uncaught error was thrown by checking that all functions foo
, bar
and
boom
were on the stack at that moment:
[root@dev /home/jgilli/node]# mdb /home/jgilli/cores/core.node.19504
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.8.271.17
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
(1 internal frame elided)
js: boom
js: bar
js: foo
js: generatorThatThrows
js: next
(1 internal frame elided)
js: <anonymous> (as <anon>)
(1 internal frame elided)
js: <anonymous> (as Module._compile)
js: <anonymous> (as Module._extensions..js)
js: <anonymous> (as Module.load)
js: <anonymous> (as Module._load)
js: <anonymous> (as Module.runMain)
js: startup
js: <anonymous> (as <anon>)
(1 internal frame elided)
(1 internal frame elided)
native: v8::internal::_GLOBAL__N_1::Invoke+0xb3
native: v8::internal::Execution::Call+0x62
native: v8::Function::Call+0xf6
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x508
native: _start+0x6c
>
This change does not alter the semantics of throwing an uncaught exception
from within a generator, or calling the generator's throw
method, so it seems like it could be an uncontroversial change.
Catching the exception also has the same semantics with and without this change:
[jgilli@dev ~/node]$ cat ~/generators-js/test-generator-throws-catch.js
function boom() {
throw new Error('boom');
}
function bar() {
boom();
}
function foo() {
bar();
}
function* generatorThatThrows() {
yield foo();
}
var generator = generatorThatThrows();
try {
console.log(generator.next());
} catch (err) {
console.log('Error:', err);
}
[jgilli@dev ~/node]$ node --version
v4.2.1
[jgilli@dev ~/node]$ ./node --version
v6.0.0-pre # This is a local development version with the patch presented above
[jgilli@dev ~/node]$ node --abort-on-uncaught-exception ~/generators-js/test-generator-throws-catch.js
Error: [Error: boom]
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ node ~/generators-js/test-generator-throws-catch.js
Error: [Error: boom]
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ ./node --abort-on-uncaught-exception ~/generators-js/test-generator-throws-catch.js
Error: Error: boom
at boom (/home/jgilli/generators-js/test-generator-throws-catch.js:2:11)
at bar (/home/jgilli/generators-js/test-generator-throws-catch.js:6:5)
at foo (/home/jgilli/generators-js/test-generator-throws-catch.js:10:5)
at generatorThatThrows (/home/jgilli/generators-js/test-generator-throws-catch.js:14:11)
at next (native)
at Object.<anonymous> (/home/jgilli/generators-js/test-generator-throws-catch.js:20:25)
at Module._compile (module.js:417:34)
at Object.Module._extensions..js (module.js:426:10)
at Module.load (module.js:357:32)
at Function.Module._load (module.js:314:12)
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ ./node ~/generators-js/test-generator-throws-catch.js
Error: Error: boom
at boom (/home/jgilli/generators-js/test-generator-throws-catch.js:2:11)
at bar (/home/jgilli/generators-js/test-generator-throws-catch.js:6:5)
at foo (/home/jgilli/generators-js/test-generator-throws-catch.js:10:5)
at generatorThatThrows (/home/jgilli/generators-js/test-generator-throws-catch.js:14:11)
at next (native)
at Object.<anonymous> (/home/jgilli/generators-js/test-generator-throws-catch.js:20:25)
at Module._compile (module.js:417:34)
at Object.Module._extensions..js (module.js:426:10)
at Module.load (module.js:357:32)
at Function.Module._load (module.js:314:12)
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$
The only difference is in the way the Error
object is displayed on the
console, which I'm still investigating.
Another (maybe less) potentially problematic characteristic of generators with regards to post-mortem debugging is that, even though they are stateful, their state cannot be inspected by post-mortem debugging tools.
Let's consider the following sample code:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
aGenerator.throw();
When loading the generator core file, there is no way to determine that the
generator threw on the second yield
statement:
$ node --abort-on-uncaught-exception generator-state.js
{ value: 1, done: false }
Uncaught Error: Foo
FROM
Object.<anonymous> (/home/jgilli/generator-state.js:9:17)
Module._compile (module.js:435:26)
Object.Module._extensions..js (module.js:442:10)
Module.load (module.js:356:32)
Function.Module._load (module.js:311:12)
Function.Module.runMain (module.js:467:10)
startup (node.js:134:18)
node.js:961:3
Illegal Instruction (core dumped)
$ mdb /home/jgilli/cores/core.node.20332
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack -van0
fffffd7fffdfedf0 v8::base::OS::Abort+9
fffffd7fffdff0d0 v8::internal::Runtime_ResumeJSGeneratorObject+0xe1
fffffd7fffdff0f0 0x38daaf40b61b internal (Code: 38daaf40b561)
fffffd7fffdff128 0x38daaf54cd6d myGenerator (JSFunction: 3e0fd3005721)
file: /home/jgilli/generators-js/generator-state.js
posn: line 1
this: 3650e210b181 (JSGlobalProxy)
fffffd7fffdff158 0x38daaf5818b3 throw (JSFunction: 32f0102d23a1)
file: native generator.js
posn: position 748
this: 3e0fd3005671 (JSGeneratorObject)
arg1: 3e0fd308a2a9 (JSObject: Error)
fffffd7fffdff1a8 0x38daaf54befb <anonymous> (as <anon>) (JSFunction: 3e0fd3005769)
file: /home/jgilli/generators-js/generator-state.js
posn: line 1
this: 3e0fd3005921 (JSObject: Object)
arg1: 3e0fd3005921 (JSObject: Object)
arg2: 3e0fd30058d9 (JSFunction)
arg3: 3e0fd3005851 (JSObject: Module)
arg4: 3e0fd30057d9 (SeqTwoByteString)
arg5: 3e0fd30057b1 (ConsString)
fffffd7fffdff218 0x38daaf436163 <InternalFrame>
fffffd7fffdff290 0x38daaf54b2e1 <anonymous> (as Module._compile) (JSFunction: 9100af3d2d1)
file: module.js
posn: line 380
this: 3e0fd3005851 (JSObject: Module)
arg1: 3e0fd3005bb9 (SeqOneByteString)
arg2: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff2d8 0x38daaf54470b <anonymous> (as Module._extensions..js) (JSFunction: 9100af3d379)
file: module.js
posn: line 424
this: 3e0fd3005cc1 (JSObject: Object)
arg1: 3e0fd3005851 (JSObject: Module)
arg2: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff320 0x38daaf541a6f <anonymous> (as Module.load) (JSFunction: 9100af3d1a1)
file: module.js
posn: line 348
this: 3e0fd3005851 (JSObject: Module)
arg1: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff380 0x38daaf5379f2 <anonymous> (as Module._load) (JSFunction: 9100af3d019)
file: module.js
posn: line 285
this: 3e0fd3005d91 (JSFunction)
arg1: 3e0fd3005d19 (SeqTwoByteString)
arg2: 32f010204101 (Oddball: "null")
arg3: 32f010204231 (Oddball: "true")
fffffd7fffdff3c8 0x38daaf5373e6 <anonymous> (as Module.runMain) (JSFunction: 9100af3d571)
file: module.js
posn: line 449
this: 3e0fd3005d91 (JSFunction)
fffffd7fffdff450 0x38daaf442d8a startup (JSFunction: 3650e210b1a9)
> 3e0fd3005671::jsprint
mdb: <unknown JavaScript object type "JSGeneratorObject">
> 3e0fd3005671::v8print
3e0fd3005671 JSGeneratorObject {
3e0fd3005671 JSObject {
3e0fd3005671 JSReceiver {
3e0fd3005671 HeapObject < Object {
3e0fd3005670 map = 72dca119449 (Map)
}
}
3e0fd3005678 properties = 32f010204139 (FixedArray)
3e0fd3005680 elements = 32f010204139 (FixedArray)
}
3e0fd3005688 function = 3e0fd3005721 (JSFunction)
3e0fd3005690 context = 3e0fd3070fd1 (FixedArray)
3e0fd3005698 receiver = 3650e210b181 (JSGlobalProxy)
3e0fd30056a0 continuation = ffffffff00000000 (SMI: value = -1)
3e0fd30056a8 operand_stack = 32f010204139 (FixedArray)
}
>
When a generator reaches a yield
statement, it stores the current offset
from the start of the generated generator function into the continuation
property. Having access to that value would be a first step in understanding in what state the generator was when it threw an error.
With the ::v8print
command, we can inspect the value of the continuation
accessor. However we can see from the output above that it only indicates that
the generator is executing (-1 === kGeneratorExecuting
). This value is not
sufficient to determine in what state the generator was when it threw an
error.
Now consider the following similar code written without using ES6 generators:
function myGenerator() {
var state = {value: undefined, done: false};
return {
next: function next() {
if (state.value === undefined || state.value < 3) {
state.value = (state.value !== undefined ? ++state.value : 1);
} else {
state.done = true
}
return state;
},
throw: function doThrow(err) {
state.done = true;
if (err === undefined) {
err = new Error('Foo');
}
throw err;
}
}
}
var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
aGenerator.throw();
When running this code with the --abort-on-uncaught-exception
command line
switch, it is possible to inspect the encapsulated state
by accessing the
generator's throw
function's closure:
$ node --abort-on-uncaught-exception custom-generator-state.js
{ value: 1, done: false }
Uncaught Error: Foo
FROM
Object.doThrow [as throw] (/home/jgilli/generators-js/custom-generator-state.js:20:7)
Object.<anonymous> (/home/jgilli/generators-js/custom-generator-state.js:27:17)
Module._compile (module.js:435:26)
Object.Module._extensions..js (module.js:442:10)
Module.load (module.js:356:32)
Function.Module._load (module.js:311:12)
Function.Module.runMain (module.js:467:10)
startup (node.js:134:18)
node.js:961:3
Illegal Instruction (core dumped)
$ mdb /home/jgilli/cores/core.node.20197
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
(1 internal frame elided)
js: doThrow
(1 internal frame elided)
js: <anonymous> (as <anon>)
(1 internal frame elided)
js: <anonymous> (as Module._compile)
js: <anonymous> (as Module._extensions..js)
js: <anonymous> (as Module.load)
js: <anonymous> (as Module._load)
js: <anonymous> (as Module.runMain)
js: startup
js: <anonymous> (as <anon>)
(1 internal frame elided)
(1 internal frame elided)
native: v8::internal::Execution::Call+0x107
native: v8::Function::Call+0xff
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x4e8
native: _start+0x6c
> ::jsstack -a
fffffd7fffdff110 v8::base::OS::Abort+9
fffffd7fffdff140 v8::internal::Runtime_Throw+0x30
fffffd7fffdff168 0x1bd1b00060bb internal (Code: 1bd1b0006001)
fffffd7fffdff198 0x1bd1b00e3afd doThrow (JSFunction: 36afff087e19)
fffffd7fffdff1d0 0x1bd1b0019494 <ArgumentsAdaptorFrame>
fffffd7fffdff210 0x1bd1b00bf3d1 <anonymous> (as <anon>) (JSFunction: 36afff0879c9)
fffffd7fffdff278 0x1bd1b001f5c7 <InternalFrame>
fffffd7fffdff2e0 0x1bd1b00beab7 <anonymous> (as Module._compile) (JSFunction: fca96abb4c9)
fffffd7fffdff328 0x1bd1b00b710b <anonymous> (as Module._extensions..js) (JSFunction: fca96abb541)
fffffd7fffdff370 0x1bd1b00b3a72 <anonymous> (as Module.load) (JSFunction: fca96abb439)
fffffd7fffdff3d8 0x1bd1b00a9b30 <anonymous> (as Module._load) (JSFunction: fca96abb351)
fffffd7fffdff420 0x1bd1b00a9263 <anonymous> (as Module.runMain) (JSFunction: fca96abb649)
fffffd7fffdff4b0 0x1bd1b007a083 startup (JSFunction: fca96a5a7f9)
fffffd7fffdff4e8 0x1bd1b0075d92 <anonymous> (as <anon>) (JSFunction: 2122fe5b84f9)
fffffd7fffdff528 0x1bd1b0019f7d <InternalFrame>
fffffd7fffdff590 0x1bd1b00189e2 <EntryFrame>
fffffd7fffdff630 v8::internal::Execution::Call+0x107
fffffd7fffdff6d0 v8::Function::Call+0xff
fffffd7fffdff700 v8::Function::Call+0x41
fffffd7fffdff7f0 node::LoadEnvironment+0x1e8
fffffd7fffdff930 node::Start+0x4e8
fffffd7fffdff940 _start+0x6c
> 36afff087e19::jsclosure
"state": 36afff087c31: {
"value": 100000000: 1,
"done": 2122fe5043f1: true,
}
>
From the output above we can determine in what state the generator was when it threw an error.
TODO