Search code examples
javascriptasync-awaittry-catch

Try/catch async function outside of async context


I have a few classes which use 'dns' from node.js. But when an error occurs, my app is thrown. I made a siimple example with classes and throwable functions and I faced with the same problem. It's works if an exception is thrown from function but it doesn't work if an exception is thorwn from class. Example:

class Test {
  constructor() {
    this.t();
  }
  async t() {
    throw new Error("From class");
  }
}

async function test(){
  new Test();
}

try {
  test().catch(e => {
    console.log("From async catch");
  });
} catch (e) {
  console.log("From try catch");
}

Output:

Uncaught (in promise) Error: From class
    at Test.t (<anonymous>:6:11)
    at new Test (<anonymous>:3:10)
    at test (<anonymous>:11:3)
    at <anonymous>:15:3

How to catch errors from try/catch block in this example?

UPD: Full code (typescript):

export class RedisService {
  client: any;
  expirationTime: any;

  constructor(args: RedisServiceOptions) {
    let redisData: any = {};
    if (args.password)
      redisData["defaults"] = { password: args.password };
    dns.resolveSrv(args.host, (err, addresses) => {
      if (err) {
        /// Handling error in main func
      }
      else {
        log.info("Using Redis cluster mode");
        redisData["rootNodes"] = addresses.map(address => {
          log.info(`Adding Redis cluster node: ${address.name}:${address.port}`);
          return Object({ url: `redis://${address.name}:${address.port}` })
        });
        this.client = createCluster(redisData);
      };
      this.client.on('error', (err: Error) => log.error(`Redis error: ${err.message}`));
      this.client.connect().then(() => { log.info("Connected to Redis") });
    });
    this.expirationTime = args.expirationTime;
  }
/// Class functions
}

Solution

  • it doesn't work if an exception is thrown from class.

    In particular, when an asynchronous error event occurs in the constructor, yes. Like your question title says, you can't handle errors outside of an async context, and a constructor is not that.

    Your current implementation has many issues, from client being undefined until it is initialised to not being able to notify your caller about errors.

    All this can be solved by not putting asynchronous initialisation code inside a constructor. Create the instance only once you have all the parts, use an async helper factory function to get (and wait for) the parts.

    export class RedisService {
      client: RedisClient;
      expirationTime: number | null;
    
      constructor(client: RedisClient, expirationTime: number | null) {
        this.client = client;
        this.expirationTime = expirationTime;
      }
    
      static create(args: RedisServiceOptions) {
        const addresses = await dns.promises.resolveSrv(args.host);
        log.info("Using Redis cluster mode");
        const redisData = {
          defaults: args.password ? { password: args.password } : undefined,
          rootNodes: addresses.map(address => {
            log.info(`Adding Redis cluster node: ${address.name}:${address.port}`);
            return { url: `redis://${address.name}:${address.port}` };
          }),
        };
        const client = createCluster(redisData);
        client.on('error', (err: Error) => log.error(`Redis error: ${err.message}`));
        await this.client.connect();
        log.info("Connected to Redis");
        return new RedisService(client, args.expirationTime);
      }
      … // instance methods
    }
    

    Now in your main function, you can call create, use await, and handle errors from it:

    async function main(){
      try {
        const service = await RedisService.create(…);
      } catch(e) {
        console.log("From async catch", e);
      }
    }