10

I have this sort of lazy initialization code in my program:

let user = null;        
let getUser = async () => {
  if(!user) {
    user = await getUserSomehow();
  }
  return user;
};

I understand that it is not safe, due to a possible race condition if I have the next code:

// one place of the program
let u1 = await getUser();
...
// another place of the program running during getUserSomehow() for u1 still hasn't finished
let u2 = await getUser();

getUserSomehow() will be called two times instead of one.

How to avoid this situation?

Arsenii Fomin
  • 3,120
  • 3
  • 22
  • 42
  • 1
    Just drop the `await`. – Bergi Nov 11 '19 at 07:43
  • @Bergi your comment is so short that at first glance it looks stupid (like you are advising to get rid of async code). However, if it's not planned to use "user" variable directly anywhere, you are completely right. – Arsenii Fomin Nov 11 '19 at 09:47

1 Answers1

8

When called for the first time, assign a Promise instead, and on further calls, return that Promise:

let userProm = null;
let getUser = () => {
  if (!userProm) {
    userProm = getUserSomehow();
  }
  return userProm;
};

Even better, scope userProm only inside getUser, to be safer and clear:

const getUser = (() => {
  let userProm = null;
  return () => {
    if (!userProm) {
      userProm = getUserSomehow();
    }
    return userProm;
  };
})();

const getUserSomehow = () => {
  console.log('getting user');
  return Promise.resolve('data');
};

const getUser = (() => {
  let userProm = null;
  return () => {
    if (!userProm) {
      userProm = getUserSomehow();
    }
    return userProm;
  };
})();

(async () => {
  const userProm1 = getUser();
  const userProm2 = getUser();
  Promise.all([userProm1, userProm2]).then(() => {
    console.log('All done');
  });
})();

Your existing code happens to be safe, because the assignment to user will occur before the first call of getUser finishes:

const getUserSomehow = () => {
  console.log('Getting user');
  return Promise.resolve('data');
};

let user = null;
let getUser = async() => {
  if (!user) {
    user = await getUserSomehow();
  }
  return user;
};

(async () => {
  let u1 = await getUser();
  let u2 = await getUser();
  console.log('Done');
})();

But it wouldn't be if the Promises were initialized in parallel, before one of them was awaited to completion first:

const getUserSomehow = () => {
  console.log('Getting user');
  return Promise.resolve('data');
};

let user = null;
let getUser = async() => {
  if (!user) {
    user = await getUserSomehow();
  }
  return user;
};

(async() => {
  let u1Prom = getUser();
  let u2Prom = getUser();
  await Promise.all([u1Prom, u2Prom]);
  console.log('Done');
})();

As shown above, assigning the Promise to the persistent variable (instead of awaiting the value inside getUser) fixes this.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Thank you for the several examples providing better understanding! You are right that in my original post I have had a wrong example about getting the race condition. – Arsenii Fomin Nov 11 '19 at 09:54
  • 1
    Very nicely answered. I'm surprised this issue doesn't come up more frequently. Another practice I follow is to keep my promise references at the top of the module (so I can keep track of them) and implement a "getter" and "resetter" for each. A disadvantage of the inner scope is: no ability to write that "resetter." – Robert May 22 '20 at 20:33