Skip to content

Event loop issue #8972

Closed
Closed

Description

@ScreamZ

What version of Bun is running?

1.0.25+a8ff7be64

What platform is your computer?

Darwin 23.0.0 arm64 arm

What steps can reproduce the bug?

Hi,

I'm not sure if I'm reporting a bug or what, but as I'm trying to understand each phase of the event loop correctly, I concede this is an edge case but why is that behavior not the same as NodeJS.

I'm learning from https://www.builder.io/blog/visualizing-nodejs-io-polling and they say for the following example

const fs = require("fs");

fs.readFile(__filename, () => {
  console.log("this is readFile 1");
});

process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
setTimeout(() => console.log("this is setTimeout 1"), 0);
setImmediate(() => console.log("this is setImmediate 1"));

for (let i = 0; i < 2000000000; i++) {}

If you run the code snippet, you may notice that the output is not what you expected. The callback message from setImmediate() is logged before the callback message from readFile() . Here is the output for reference.

I ran this code in NodeJS and got the same output. I was surprised at first as it seemed counterintuitive.

But I kept reading

Now comes the interesting part. When the control reaches the I/O queue, we expect the readFile() callback to be present, right? After all, we have a long-running for loop, and readFile() should have been completed by now.

However, in reality, the event loop has to poll to check if I/O operations are complete, and it only queues up completed operation callbacks. This means that when the control enters the I/O queue for the first time, the queue is still empty.

The control then proceeds to the polling part of the event loop, where it checks with readFile() if the task has been completed. readFile() confirms that it has, and the event loop now adds the associated callback function to the I/O queue. However, the execution has already moved past the I/O queue, and the callback has to wait for its turn to be executed.

It makes sense now! And this is logic.

I ran the same command in bun and the output was different, so why isn't bun having the same behavior? I also noticed that Deno is having the same behavior as Bun (posted example at the end of this post).

Bun

  • At the start, Bun seems to process the microtasks queue before the next tick queue, whereas Node does the opposite.
  • Then Bun can process the I/O queue, without having to poll?

I'm not a fervent defender of NodeJS who complains that the API isn't 100% compatible. But could this create potential issues?

Also from NodeJS learn section https://nodejs.org/en/learn/asynchronous-work/understanding-setimmediate

A process.nextTick callback is added to process.nextTick queue. A Promise.then() callback is added to promises microtask queue. A setTimeout, setImmediate callback is added to macrotask queue.

Event loop executes tasks in process.nextTick queue first, and then executes promises microtask queue, and then executes macrotask queue.

👀 From that point of view, the order should be: nextTick -> promises microtasks -> macrotasks but this is not the case in Bun / Deno ❗️

Thanks for lightning me up :) And happy to help with bug tracking. (if this is one)

Regards

What is the expected behavior?

this is process.nextTick 1
this is Promise.resolve 1
this is setTimeout 1
this is setImmediate 1
this is readFile 1   

What do you see instead?

this is Promise.resolve 1
this is process.nextTick 1
this is readFile 1
this is setTimeout 1
this is setImmediate 1    

Additional information

Watch this: https://www.builder.io/blog/visualizing-nodejs-io-polling

What happen on 🦕 DENO ?

import fs from "node:fs";
import { fileURLToPath } from "node:url";
import * as process from "node:process";
import { setTimeout, setImmediate } from "node:timers";

const __filename = fileURLToPath(import.meta.url);

fs.readFile(__filename, () => {
  console.log("this is readFile 1");
});

process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
setTimeout(() => console.log("this is setTimeout 1"), 0);
setImmediate(() => console.log("this is setImmediate 1"));

for (let i = 0; i < 2000000000; i++) {}

Output, same as bun

this is Promise.resolve 1
this is process.nextTick 1
this is readFile 1
this is setTimeout 1
this is setImmediate 1   

Activity

  1. ScreamZ commented on Feb 19, 2024

    @ScreamZ
    Author

    @uNetworkingAB Why did you remove your full explanation?

  2. uNetworkingAB commented on Feb 19, 2024

    @uNetworkingAB

    Changed my mind re. participating in the discussion

  3. omerbn commented on Aug 29, 2024

    @omerbn

    Guys, is this considered a bug?
    If so, is it possible that I'll try to resolve this?

  4. ScreamZ commented on Sep 1, 2024

    @ScreamZ
    Author

    Guys, is this considered a bug? If so, is it possible that I'll try to resolve this?

    For me it is. For Deno maintainers,

    Ordering of I/O completions in the macrotask queue is not guaranteed to be ordered in any specific way.

    Which I strongly disagreed. But I guess they are lazy to reimplement.

  5. uNetworkingAB commented on Sep 1, 2024

    @uNetworkingAB

    Honestly, if you rely on async completion ordering you're not writing very good code. You're breaking the whole premise of async.

  6. KnorpelSenf commented on Nov 26, 2024

    @KnorpelSenf

    They only make a statement about macrotasks. Your issue says that microtasks are not ordered correctly, either. Is that still reproducible on any runtime?

  7. robobun commented on Mar 25, 2026

    @robobun
    Collaborator

    This has been fixed — process.nextTick now fires before Promise.resolve microtasks, matching Node.js behavior on current main (v1.3.11). Closing.

  8. KnorpelSenf commented on Mar 25, 2026

    @KnorpelSenf

    Ehhh can any human confirm?

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingconfirmed bugWe can reproduce this issue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions