Skip to content

Nodejs does not wait for promise resolution - exits instead #22088

Description

@ddeath
  • Version: v10.6.0
  • Platform: Linux 4.15.0-24-generic GitHub issue management #26~16.04.1-Ubuntu SMP Fri Jun 15 14:35:08 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
  • Subsystem:

The code bellow will end with output:

Looping
before
working

and then it ends. The expected output is:

Looping
before
working

and the node should not exit

function sleep(ms) {
    return new Promise((resolve) => { setTimeout(resolve, ms) })
}

class TestClass {
    async processData(data) {
        return new Promise((resolver, rejecter) => {
            console.log('working')
        })
    }
}

class RunnerClass {

    async start() {
        const instance = new TestClass()
        while (true) {
            console.log('Looping')
            if (true) {
                try {
                    console.log('before')
                    const processedData = await instance.processData('data')
                    console.log('after')
                } catch (error) {
                    console.log('errored')
                }
            }
            await sleep(1000)
        }
    }
}

async function run() {
    const rebalancer = new RunnerClass()
    await rebalancer.start()
    console.log('end')
}

run()

If the if statment is set to if (false) then the process is outputting:

Looping
Looping
Looping
...

and never ends...

Activity

  1. vsemozhetbyt commented on Aug 2, 2018

    @vsemozhetbyt
    Contributor

    If I am not mistaken, Node.js does not wait for ever-pending Promises:

    'use strict';
    
    (async function main() {
      await new Promise((resolve) => { console.log(42); });
    })();
    42
    [exit]
    
  2. jiripospisil commented on Aug 2, 2018

    @jiripospisil

    If I am not mistaken, Node.js does not wait for ever-pending Promises

    In other words, the mere existence of a Promise won't keep the process alive. You actually need to put something on the event loop (e.g. create a timer in the processData method).

    By the way, even if the process stayed alive because of some other I/O, I think the expected output would be

    Looping
    before
    working
    

    because the promise Executor is invoked imediately.

  3. apapirovski commented on Aug 2, 2018

    @apapirovski
    Contributor

    The answers above are correct. Going to close this out.

  4. ddeath commented on Aug 2, 2018

    @ddeath
    Author

    @jiripospisil yes you are right, I edited issue. Of course that working is expected too.

    @apapirovski but why the node end the process in the middle of nowhere? It has other code to execute after that await.

    There is console.log('end') on the end. In this case I would expect that the process will throw en error or exits with some kind of error.

  5. ddeath commented on Aug 2, 2018

    @ddeath
    Author

    For example:

    'use strict';
    
    (async function main() {
      await new Promise((resolve) => { console.log(42); });
      console.log(`
        This will never be executed is this expected?
        Also node exits as everything is ok with exit code 0...
      `);
    })();
  6. jiripospisil commented on Aug 3, 2018

    @jiripospisil

    I agree it's a bit weird when you encounter it for the first time (and I have certainly been bitten by this in the past) but the way to think about it is that when you await something, V8 jumps out of the current function (I believe it's implemented as syntax sugar on top of generators, or at least that's the high level idea). At this point Node.js doesn't care about the function anymore and checks whether there are some pending tasks on the event loop. If not, it exits. If you did this instead:

    await new Promise((resolve) => { console.log(42); setTimeout(resolve, 2000) });

    then Node.js would see there's a timer and stayed alive. When the timer goes off, Node.js invokes the callback and V8 jumps back to the place from which it jumped out off and starts executing the rest of the function.

  7. Dzenly commented on Nov 9, 2018

    @Dzenly
    Contributor

    Maybe we could have warning: "Process is exited, but you have pending promises, created on the file:xxx:yyy". But of course if will require some Promise manager and must be optional, just for debug.

  8. tbranyen commented on Feb 20, 2019

    @tbranyen

    This bit me pretty hard and has been extremely difficult to understand the behavior implications. I have a bundler which operates in a giant async Promise chain. Once a task completes it moves on to the next. I've added the ability for "loaders" to pre-process an AST and wanted to use a browserify plugin. I wrote the following code:

          transform: async ({ state, source }) => {
            return new Promise((resolve, reject) => {
              const stream = combynify(state.path, {});
    
              stream.on('data', console.log);
    
              stream.resume();
              stream.on('finish', function(d) {
                resolve(d);
              });
    
              stream.on('error', err => reject(err));
            });

    This is injected into the Promise chain. Since I'm doing something wrong in this code, there is nothing on the event loop and immediately after this code executes, my bundler dies. This seems incredibly bad for an end user. How would they know what was wrong when they write such a loader? My bundler exits with no errors, exit code 0, and exits in the middle of this giant promise chain.

    Should it be my bundlers responsibility to set a huge long timer before any loader, and once the loader is done, call clearTimeout?

  9. tbranyen commented on Feb 20, 2019

    @tbranyen

    This is how I've been able to "solve" the problem from my bundler's perspective:

        if (loaderChain.length) {
          const warningTimer = setTimeout(() => {
            console.log('Loader has not resolved within 5 seconds');
          }, 5000);
          const timer = setTimeout(() => {}, 999999);
    
          code = await handleTransform({ code, loaderChain });
    
          clearTimeout(timer);
          clearTimeout(warningTimer);
        }
  10. devsnek commented on Feb 20, 2019

    @devsnek
    Member

    @tbranyen as long as the stream you wrap is doing things it will work. if the stream absolutely stops but doesn't end or error, nothing will be keeping the process open anymore.

  11. tbranyen commented on Feb 20, 2019

    @tbranyen

    @devsnek I believe I covered that in my initial comment:

    Since I'm doing something wrong in this code, there is nothing on the event loop and immediately after this code executes, my bundler dies.

    The point here is that something was wrong, and given that everything in the chain was a Promise which either resolves, rejects, or hangs, this behavior feels out-of-place. If you were using my tool and there was no indication that the loader was successful or not (since it never resolves or rejects), and the tool simply dies with a non-error exit code, that's pretty bad for debugging. In a browser, Promises can just hang indefinitely since there is no "process" to close in the web page. In Node, I can see how this behavior is ambiguous since there is a distinction made in Node as to whether v8 or libuv scheduled events determine if the Node process is out of work or not.

  12. axkibe commented on Oct 13, 2019

    @axkibe
    Contributor

    conceptionally speaking this is not good. IMO node should definitely wait until all promises resolved/rejected.

    I guess it is this way, because it is very difficult to implement? As promises are a V8 internal thing and the node.js mother process doesn't know about them?

  13. 21 remaining items

  14. axkibe commented on Jan 2, 2023

    @axkibe
    Contributor

    Anyway, as long nothing listens anymore (including all the middlewares) to any network event, file event, timer event etc. and there is nothing in the "nextTick" stack either etc. and the code passes control back to the event loop.. other than dying would be halting/zombing forever.

    AFAIK, there was a rare bug in some cases, where node died, albeit there was still something incoming from a stream.. but it has been fixed.

  15. tbranyen commented on Jan 2, 2023

    @tbranyen

    Anyway, as long nothing listens anymore (including all the middlewares) to any network event, file event, timer event etc. and there is nothing in the "nextTick" stack either etc. and the code passes control back to the event loop.. other than dying would be halting/zombing forever.

    AFAIK, there was a rare bug in some cases, where node died, albeit there was still something incoming from a stream.. but it has been fixed.

    This is precisely what happens in the browser and allows you to easily inspect the stack and current runtime, not sure why this would be controversial for Node on the command line. If you were to try and use --inspect the whole process would die before you even knew where to start.

  16. axkibe commented on Jan 3, 2023

    @axkibe
    Contributor

    Does it really close while you have an --inspect debugger enabled, because in that case it shouldn't, since its an event source (debugger attaches). If it does, you should file a bug. If it doesn't, its IMO fine. As said, otherwise it would be a zombie process.

  17. bnoordhuis commented on Jan 3, 2023

    @bnoordhuis
    Member

    I think @tbranyen is saying JS in the browser stays around so you can attach a debugger.

    Of course node is not a browser. A --hang-around-as-long-as-there-are-unresolved-promises flag is conceivable for debugging (because that's all that it's good for) but not as the default behavior.

    I'm not going to reopen this issue - too much meandering discussion - but if someone wants to open a new issue and back-link to this one, go ahead.

  18. axkibe commented on Jan 3, 2023

    @axkibe
    Contributor

    And generally speaking, I can see a point for having node hanging around for a debugger to connect even after it is finished in conventional sense .. don't need a flag for promises for this, generally speaking inspecting data structures even after it's done.

    On the other hand, this is easy to workaround, just make a dummy listener to any port and it will keep running.

    With this on the head of the root file, @tbranyen should get what they want.
    if(require('inspector').url()) require('net').createServer().listen(9999);

    I agree, I don't see any issue as of today.

  19. benjamingr commented on Feb 19, 2023

    @benjamingr
    Member

    We ran into this with the test runner today. I think we should revisit our choice to end the process with promises pending.

    In our case - we had a test that spawned a child process and transformed its stdout. Its stdout was transformed with async code which meant the parent process exited and did not wait to validate the output.

    This means code like

    const result = await fs.createReadStream("./file.txt").map(async (x) => { return await transformAsync(x); }).toArray();
    // code below never runs, because nothing is keeping the process alive between the stream being finished 
    // and the transforms/toArray which run after a microtick.
    doSomethingWith(result);  
  20. axkibe commented on Feb 20, 2023

    @axkibe
    Contributor

    benajmingr, your code seems faulty, since doSomething() is executed right away, there is nothing waiting for the stream you created to finish. And "result" is a promise you need to wait somewhere for.

    
    async function transformAsync( x )
    {
    	return '(' + x + ')';
    }
    
    async function run( )
    {
    	const result = fs.createReadStream("./file.txt").map(async (x) => { return await transformAsync(x); }).toArray( );
    	console.log( 'dosomthing', await result );
    }
    
    run( );
    

    Works fine.

  21. benjamingr commented on Feb 20, 2023

    @benjamingr
    Member

    Yeah the "this code doesn't run" bit was obviously missing an await :)

  22. josbert-m commented on May 23, 2024

    @josbert-m

    I know this discussion is closed, but I'm working on a Nest.js application and creating a custom ConfigModule similar to the one provided by the framework, as I need additional custom logic. I ran into this issue when trying to resolve a Promise from outside of its scope.

    The strange thing is, in the existing Nest.js ConfigModule, there's a similar Promise that does work:

    https://github.com/nestjs/config/blob/bee951799db96c7778c1c33914ad88202bac8768/lib/config.module.ts#L47

    Does anyone know why it works there? I'm very confused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    promisesIssues and PRs related to ECMAScript promises.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions