Search code examples
javascriptnode.jspromisecallback

Difference between callback and promise when using `net.connect`


I've built a simple script to detect if a port is opened/closed on my machine. I've successfully make it work using the old-fashion callback way, but can't make it work with Promise (async/await).

Any clue why the two scripts below are not working exactly the same? Using callbacks, it works neat. Using Promise (async/await) it crash by throwing an "uncaughtException" error.

✅ Using callbacks

const net = require('node:net')

/**
 * Connect to port with callback
 */

function connectToPort(port, callback) {
  const client = net.connect(port, 'localhost', function () {
    callback(true)
  })

  client.on('error', function (err) {
    callback(err)
  })
}

/**
 * Connect
 */

async function test(port) {
  console.log(`Trying to connect to port "${port}"...`)

  // Connect with callback
  connectToPort(port, function (result) {
    console.log({ port, open: result === true })
  })
}

// Output:
// Trying to connect to port "4242"...
// { port: 4242, open: false }
test(4242)

❌ Using Promise (async/await)

const net = require('node:net')

/**
 * Connect to port with Promise
 */

function asyncConnectToPort(port) {
  return new Promise(function (resolve, reject) {
    const client = net.connect(port, 'localhost', function () {
      resolve(true)
    })

    client.on('error', function (err) {
      reject(err)
    })
  })
}

/**
 * Connect
 */

async function test(port) {
  console.log(`Trying to connect to port "${port}"...`)

  // Connect with promise
  const result = await asyncConnectToPort(port)
  console.log({ port, open: result === true })
}

// Output:
// Trying to connect to port "4242"...
// Error: connect ECONNREFUSED 127.0.0.1:4242
test(4242)

Both scripts look exactly the same to me. Apparently, the "error" event must be present to avoid Nodejs to throw an "uncaughtException". That "special" event is detected when using callback, but I suspect it's not with Promise. Could it be something behind the scene that differs when working with await/async script?


Solution

  • An error event is raised in both your code examples.

    In the Promise code that you wrote you pass that error event to reject().

    When you call reject you raise an exception.

    Hence, you get an exception in your Promise based code but not in the other code. You added one!.

    Handling it is a case of:

    try {
      const result = await asyncConnectToPort(port)
      console.log({ port, open: result === true })
    } catch (e) {
      // do something with e here
    }
    

    However a Promise can only be settled once.

    The code you've written will call resolve when the callback to net.connect runs and it will call reject on an error event but these are not mutually exclusive.

    It's possible for your code to end up calling both resolve and reject, and also for it to call reject multiple times.