2

I'm a bit new working on big projects with JS and I feel it's really hard to keep my code clean with almost everything being async.
I'm creating a lot of Promises and declared almost every function as async and using await on almost every line, and I feel this is not the correct way to manage it.
Example:

    var mysql = require('mysql');
module.exports = class MyClass {

    constructor() {

    }

    async init(){
        await this._initDbConnection();
    }

    _initDbConnection(){
        return new Promise(function(resolve, reject){
            this.db = mysql.createConnection({
                ...
            });
            this.db.connect(function(err) {
                ...    
            });    
        });
    }

    tableExists(tableName){
        return new Promise...            
    }

    createTable(tableName){
        return new Promise...
    }
    async save(data){
        try{
            if( ! (await this.tableExists()) ){
                await this.createTable();
            }
            return new Promise(function(resolve, reject){
                this.db.query(sql, function (err, result) {
                    ...                    
                });
            });
        }
        catch(e){

        }
    }

};

const myclass = new MyClass();
await myclass.init();
await myclass.save();
await 
await 
await !

The same for every query, or anything executing async.
It's a really ugly solution.
I mean, if I need something from the DB, I want to connect to the DB in the first line, then execute the query in the second line and then process the results on the third line. With JS to do this I need to create a lot of callbacks or use await on every line???

13
  • 1
    I don't believe you can have an async class constructor. Commented Jun 27, 2019 at 1:33
  • Why are you initing the db there? Your right its a mess. Commented Jun 27, 2019 at 1:33
  • @Marty that's even worse then because I need to create some "init" function and call that with async after creating the object to make sure the DB connection is ready? Commented Jun 27, 2019 at 1:37
  • @LawrenceCherone it's just an example, the problem is not only with DB but also with anything async, and that seems to be almost everything in JS :( can you suggest a better solution for that example? Commented Jun 27, 2019 at 1:38
  • @Enrique Yep, that's standard - probably connect() would be more appropriate though. Commented Jun 27, 2019 at 1:41

3 Answers 3

2

For the very specific case of initialising an async resource there are several design patterns you can use. Note that these design patterns will not really help with other use cases of asynchronous code.

1. Init function

As you've demonstrated in your own code, this is one way to do it. Basically you have an asynchronous method to initialise your resource. This is similar to jQuery's .ready() function. There are several ways to write an init function. The most straightforward is probably to accept a callback allowing you to continue with your logic:

class Foo {
    init (callback) {
        connectToDB().then(db => {
            this.db = db;
            callback(this);
        });
    }
}

usage:

let foo = new Foo();
foo.init(async function(){
    await foo.save();
});

2. Builder pattern

This design pattern is more common in the Java world and is seen less often in javascript. The builder pattern is used when your object needs complex initialisation. Needing an asynchronous resource is exactly the kind of complexity that lends itself well to the builder pattern:

class Foo {
    constructor (db) {
        if (typeof db === 'undefined') {
            throw new Error('Cannot be called directly');
        }
        this.db = db;
    }

    static async build () {
        let db = await connectToDB();
        return new Foo(db);
    }
}

usage:

Foo.build().then(foo => {
    foo.save();
});

3. On-demand initialisation / hidden init

This design pattern is useful if your initialisation is messy or complicated and you'd prefer a cleaner API. The idea is to cache the resource and only initialise it when not yet initialised:

class Foo {
    constructor () {
        this.db = null;
    }

    db () {
        if (this._dbConnection !== null) {
            return Promise.resolve(this._dbConnection);
        }
        else {
            return connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
    }

    async save (data) {
        let db = await this.db();
        return db.saveData(data);
    }

}

usage:

async function () {
    let foo = new Foo();
    await foo.save(something);  // no init!!
    await foo.save(somethingElse);
}

Bonus

If you look back at the init function example you will see that the callback looks kind of like a control structure - kind of like a while() or if(). This is one of the killer features of anonymous functions - the ability to create control structures. There are good examples of this in standard javascript such as .map() and .forEach() and even good-old .sort().

You are free to create asynchronous control structures (the coalan/async and async-q libraries are good examples of this). Instead of:

if( ! (await this.tableExists()) ) { ...

You can write it as:

this.ifTableNotExist(()=>{
    return this.createTable();
})
.then(()=>{ ...

possible implementation:

  ifTableNotExist (callback) {
      return new Promise((ok,err) => {
          someAsyncFunction((table) => {
              if (!table) ok(callback());
          });
      });
  }

async/await is just one tool in asynchronous programming. And is itself a design pattern. Therefore limiting yourself to async/await limits your software design. Get comfortable with anonymous functions and you will see lots of opportunities for refactoring asynchronous code.

Bonus the 2nd

In the example for the on-demand init pattern the usage example saves two pieces of data sequentially by using await. This was because the code would initialise the db connection twice if we don't wait for it to complete.

But what if we want to speed up the code and perform both saves in parallel? What if we want to do this:

// Parallel:
await Promise.all([
    foo.save(something),
    foo.save(somethingElse)
]);

What we can do is we can have the .db() method check if there's a pending promise:

// method to get db connection:
db () {
    if (this._dbConnection !== null) {
        return Promise.resolve(this._dbConnection);
    }
    else {
        if (this._dbPromise === null) {
            this._dbPromise = connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
        return this._dbPromise;
    }
}

In fact, since there's no limit to how many times we can call .then() on a Promise, we can actually simplify that and just cache the promise (don't know why I didn't think of it before):

// method to get db connection:
db () {
    if (this._dbPromise === null) {
        this._dbPromise = connectToDB();
    }
    return this._dbPromise;
}
Sign up to request clarification or add additional context in comments.

7 Comments

On 3) When you use "let db = this.db();" should not be instead await this.db();? also we should not avoid somehow initializing the DB multiple times? suppose this.db() is called multiple times before it finish the DB connection.
Yes! Nice catch
The idea of (3) is that we initialise db connection only once and then cache it. But the initialisation is hidden by using the same function to both initialise the connection and return the cached connection
Yes but what happen if we execute 2 consecutive save: foo.save(something); foo.save(somethingElse); the second execution will probably won't have the DB initialized so will try to initialize it again?
Not if you await it (or do the second one in .then())
|
1

db.js

const options = require('../options')
var mysql = require('mysql');

class DataBase {
  constructor(options){
    this.options = options
    this.db = mysql.createConnection(this.options)
  }

  connect(){
    if(this.db.connected){
      return Promise.resolve(this.db)
    }
    return new Promise(function(resolve, reject){
      this.db.connect(function(err) {
        if (err) {
          reject(err);
        } else {
          console.log("Connected to MySQL!");
          resolve(this.db);
        }
      });  
    })
  }
}

module.exports = new Database(options)

index.js

const db = require('./db')

db.connect()

anywhere.js

 const db = require('../db')

 async function(){
   await db.connect()
   db.db.doWhatever()
 }

Obviously you only need the redundants await db.connect() in operations you wish to do at start up so, in routes, for example, you already know it is connected from the launch:

routes.js

const db = require('../db').db

app.get('/posts', async(req, res) => {
  const posts = await db.query('select * from posts')
  res.send(posts)
}

Comments

1

If something is async you have to handle it anyway either with "then" async/wait or callbacks. Now the fact you have "classes" in JavaScript doesn't mean you have to use them. I'm not a big fan of classes and Classical OOP.
I write things differently...something people frown upon but anyway such is life. The class you wrote it doesn't seem to have any state I don't see the point of using a class also but it's matter of preferences.
It looks like is a Service class.
A nice thing of not using classes is you don't need to prefix everything with the ugly "this" shit. You can write the code above in a Module with just functions.

Also keep in mind you don't explicitly need to return a Promise if the function is async

const { log, error } = console;

async function promiseMe(shouldIthrow) {
  if (!shouldIthrow) {
    return 'I Promise you'; //See? no Promise, it will be wrapped in a promise for you
  } else throw Error('I promise an Error')
}

// somewhere else
(async function run() {
  try {
    const result = await promiseMe(false)
    log('Look mum, a promise', result);

  } catch (r) {

  }

})();
// Or "then"
promiseMe(false).then(value => log('Look mum, a promise'));
promiseMe(true).then(_ => { }).catch(e => error('Oh men!'));

Now, this is how I would write the code you are asking for (It's actually working code, useless though)

const db = {
  query: function (sql, callback) {
    //sanitze your sql
    callback && callback({ result: 'database deleted' });
  },
  initConnection: async function () {
    !dbStarted && (dbStarted = true) && (log('DB Started'));
    return db; 
  }
}

function Completer() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { resolve, reject, promise };
}

//Higher order function to decorate anything that uses a db
// to ensure there's a db connection 
function withDb(decorated) {
  return async function decorator() {
    await  db.initConnection();
    decorated() 
  }
}
const tableExists = withDb(async function tableExists() {
  log('tableExists');
  return false ///whatever code you need here
});

async function createTable() {
  log('createTable');
  return false ///whatever code you need here
}

function saveHandler(completer){
  return function (data) {
      data.result && completer.resolve(data.result);
      data.error && completer.reject(data.result);
    }
}

async function save(data) {
  try {
    (!await tableExists()) && await createTable();

    const completer = Completer();
    db.query('DROP DATABASE databasename;', saveHandler(completer)); 

    return completer.promise;
  }
  catch (e) {
    //Nah no errors
  }
}

save('blah blah').then(result => { log('[Saved?] oh no:', result) });

// or
(async function run() {
  const result = await save('blah blah');
  log('[Saved?] oh no:', result);
})();

3 Comments

But that's what I'm talking about, look how complex that code it is! we need decorators, and functions returning functions and wrapping everything inside extra code, when in other languages we can just write that in 3 lines: "db = connect(); result = db.query(); console.log(result)" I don't get the point to make it so complex. When working on the server side we normally need everything ready on our next line of code, we are not blocking the UI like in the browser, so making everything async does not make sense to me, is just adding complexity and is more difficult to read and follow
JavaScript is async, it has an event loop to handle all this things for you. Other languages are blocking. Alternatively you can spawn another process and...wait for it... wait for the response. I guess you are not used to JS maybe you are coming from another language and is not natural to you i get it. The fact i created a decorator is optional you can do any thing else, is up to you and the knowledge you have of the language. To me is more ugly writing all the time"THIS.something" all the time.
In my case I love JS and i use all the "functional" style stuff. In my opinion, such an Amazing language but I use other language like Rust and Dart and they have some functional aspects that are more expressive/declarative in my opinion. Get to know the language better and it won't feel like this. I believe this is foreign to you now. for instance you were returning a Promise from and async function when you don't have to because the language does it for you.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.