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???

Enrique
  • 4,693
  • 5
  • 51
  • 71
  • 1
    I don't believe you can have an async class constructor. – Marty Jun 27 '19 at 01:33
  • Why are you initing the db there? Your right its a mess. – Lawrence Cherone Jun 27 '19 at 01: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? – Enrique Jun 27 '19 at 01: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? – Enrique Jun 27 '19 at 01:38
  • @Enrique Yep, that's standard - probably `connect()` would be more appropriate though. – Marty Jun 27 '19 at 01:41
  • For constructors you've already suggested the correct solution yourself. There's actually a question and answer about this - https://stackoverflow.com/questions/43431550/async-await-class-constructor/43433773#43433773 – slebetman Jun 27 '19 at 01:43
  • @Marty was just an example, I'm looking for a generic design pattern. But I think to create a "connect" function is not nice, is something internal for the class (if it's using a database, or files, or memory or whatever) – Enrique Jun 27 '19 at 01:45
  • I mean you could always do a check in every function call that needs a valid connection to see whether a connection is available and if not make the connection first, however I'd say that's considerably worse. – Marty Jun 27 '19 at 01:47
  • FWIW - lots of callbacks is not actually ugly and not actually not clean. It is in fact considered good practice in lots of other languages like Haskell and Lisp. You're just not used to seeing it. The **BEST** advice I can give you is get used to it. You will start seeing this style of development (also called evented, event-driven, continuation-passing-style etc.) in other languages like Java's Futures or Python's Twisted framework or C++'s new lambdas or goroutines. Clean is a matter of how you modularise your code, not the architecture – slebetman Jun 27 '19 at 01:48
  • As it is this question is too broad. But if you don't mind restricting it to one single concrete problem - such as design patterns to initialise asynchronous resource or how to initialise db connection then I believe that there can be a concrete answer to this – slebetman Jun 27 '19 at 01:52
  • @slebetman I'm looking for design patterns for the async/await not the database. The thing is I find myself embedding almost everything inside a Promise and declaring almost every function as async and using await in almot every line of my code so I feel I'm doing it wrong. – Enrique Jun 27 '19 at 02:01
  • If you are new to asynchronous programming I strongly suggest you try callbacks and calling `.then()` in your architecture before using async/await. Since await blocks the current function you will never get the feel of asynchronous design patterns - event emitters, recursive scheduling etc. You are in effect giving up control of your code - you are giving up the choice of which part of the code you want to wait to continue and which part you want to execute without waiting – slebetman Jun 27 '19 at 02:08
  • @slebetman I have updated the example, I think you can see better what I mean with the Promise and await problem. I feel I'm repeating myself all the time creating Promises everythwere. Can you write an aswer suggesting another solution for that example? – Enrique Jun 27 '19 at 02:10

3 Answers3

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;
}
slebetman
  • 109,858
  • 19
  • 140
  • 171
  • 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. – Enrique Jun 27 '19 at 13:15
  • Yes! Nice catch – slebetman Jun 27 '19 at 13:56
  • 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 – slebetman Jun 27 '19 at 13:58
  • 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? – Enrique Jun 27 '19 at 19:52
  • Not if you await it (or do the second one in `.then()`) – slebetman Jun 28 '19 at 03:07
  • @Enrique - See the addition I made to the answer at the bottom – slebetman Jun 28 '19 at 03:35
  • great! I think I need to change my mind in JS, maybe I need to think everything as "events", like an Observer Pattern but just using the callbacks as the observer registration – Enrique Jun 29 '19 at 14:01
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)
}
André Alçada Padez
  • 10,987
  • 24
  • 67
  • 120
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);
})();
AleC
  • 472
  • 6
  • 8
  • 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 – Enrique Jun 27 '19 at 13:03
  • 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. – AleC Jun 27 '19 at 15:37
  • 1
    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. – AleC Jun 27 '19 at 15:38