I recently came across the article Tail call optimization in ECMAScript 6. I was interested in testing the TCO behavior (Even though I later found that the TCO was not supported by nodejs 8+ as mentioned by the article) and found behavior that I could not understand.
Plain loop function
'use strict'; process.on('SIGTERM', () => { console.log('SIGTERM received'); process.exit(0); }) process.on('SIGINT', () => { console.log('SIGINT received'); process.exit(0); }) process.on('uncaughtException', (error) => { console.error('Uncaught exception', error); process.exit(1); }) process.on('unhandledRejection', (error) => { console.error('Unhandled rejection', error); process.exit(0); }) let counter = 0; function test() { console.log(`Counter: ${counter++}`); test(); } console.log('Test started'); test(); console.log('Test ended');
This version of code produces:
Test started Counter: 0 ... Counter: 10452 Uncaught exception RangeError: Maximum call stack size exceeded at WriteStream.removeListener (events.js:306:28) at write (console.js:130:12) at Console.log (console.js:135:3) at test (/test.js:31:13) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5) at test (/test.js:32:5)
Async loop function without any await within the function
'use strict'; process.on('SIGTERM', () => { console.log('SIGTERM received'); process.exit(0); }) process.on('SIGINT', () => { console.log('SIGINT received'); process.exit(0); }) process.on('uncaughtException', (error) => { console.error('Uncaught exception', error); process.exit(1); }) process.on('unhandledRejection', (error) => { console.error('Unhandled rejection', error); process.exit(0); }) let counter = 0; async function test() { console.log(`Counter: ${counter++}`); test(); } console.log('Test started'); test(); console.log('Test ended');
This version of code produces:
Test started Counter: 0 ... Counter: 6967 Test ended
Async loop function with await within the function
'use strict'; const bluebird = require('bluebird'); process.on('SIGTERM', () => { console.log('SIGTERM received'); process.exit(0); }) process.on('SIGINT', () => { console.log('SIGINT received'); process.exit(0); }) process.on('uncaughtException', (error) => { console.error('Uncaught exception', error); process.exit(1); }) process.on('unhandledRejection', (error) => { console.error('Unhandled rejection', error); process.exit(0); }) let counter = 0; async function test() { await bluebird.delay(1); console.log(`Counter: ${counter++}`); test(); } console.log('Test started'); test(); console.log('Test ended');
This version of code runs non-stop.
In Summary:
- plain loop function: stops at counter 10452 and throws RangeError: Maximum call stack size exceeded
- Async loop function without any await within the function: stops at counter 6967 without any error
- Async loop function with await within the function: runs non-stop
Can anyone explain this behavior differences or point me to any keyword i can google?