Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

Abusing promises as a locking mechanism in JavaScript

Promises in JavaScript are great for asynchronous data handling, but using them as a locking mechanism can lead to unexpected problems. Find out why and learn better approaches.
Neal Lindsay
|
May 13, 2019
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

One of my favorite things about Promises in JavaScript is that they have a well-defined and narrow purpose: you ask for data and get a Promise that will give it to you asynchronously.

Promises are not a solution for handling events. Because a Promise only resolves once, it’s not a good fit to tell you each time a button is clicked or an HTTP request comes in over the network.

However, there is a bit of useful information you can get from a Promise other than the data that will come through it: sometimes you want to know when an asynchronous operation is complete. Here’s an example of what I mean:

prompt is a node library that accepts text input in the terminal. It exposes a traditional node-callback-style API. I recently had occasion to promisify it and ran into a funny problem. This is what I initially wrote:

const prompt = require('prompt')

const promisePrompt = (name) => {
  return new Promise((resolve, reject) => {
    prompt.get(name, (err, result) => {
      if (err) {
        reject(err)
      } else {
        resolve(result[name])
      }
    })
  })
}

const fields = ['first', 'middle', 'last']

Promise
  .all(fields.map(promisePrompt))
  .then(console.log.bind(console))
  .catch(console.error.bind(console))

(If you’re a bit fuzzy on how promises work, I recorded a conference talk on JavaScript promises that you might want to watch.)

Don’t worry if you don’t see anything wrong here. The problem isn’t in the code exactly, I just made an incorrect assumption about how prompt.get() works.

At the bottom of the code example, we’re mapping across our field names and calling promisePrompt three times in a row. Then we gather those three promises back together with Promise.all(…).

Unfortunately, if you call prompt.get() a second time before the user is done typing into your first invocation, prompt will just use the same input for both. In other words, prompt is not written to be run more than once at the same time.

$ node index.js prompt: last:  LLLiiinnnddd?????? [ 'Lind??', 'Lind??', 'Lind??' ] $

When we map across our fields array, we’re calling prompt.get() three times before the user can even type a character.

Please note, this is not how prompt is supposed to be used - it wants you to pass in a list of fields instead of calling it once per field. So this example is a bit contrived, but it’s a great chance to use promises in a novel way.

What can we do to fix this?

Here is a function that takes a promise-returning function (f) and wraps it so it will wait until any previous call is finished:

const lockify = (f) => {
  var lock = Promise.resolve()
  return (...params) => {
    const result = lock.then(() => f(...params))
    lock = result.catch(() => {})
    return result
  }
}

When you call lockify:

  • It immediately creates an internal lock promise.
  • It returns a new function.

The returned function also does two things when you invoke it:

  • It creates a result promise that will get returned.
  • It overwrites lock with a new promise that waits on result.

Eventually, after the initial lock finishes, f will be called. Whenever the promise that f returns is done, result and the new lock will both resolve.

As a bonus, lockify is an existing npm package.

Why does this work?

lock is the heart of lockify and is being used in an unusual way for a promise: we never care what its value is. Instead, it’s being used exclusively for timing.

Notice that lock is the only identifier defined as a var. That’s so we can overwrite lock each time our function is called.

Also notice that we create each lock except the first by calling result.catch(() => {}). This means that lock will resolve after result, even if result was rejected.

Here is the full code for our fixed version:

const prompt = require('prompt')

const promisePrompt = (name) => {
  return new Promise((resolve, reject) => {
    prompt.get(name, (err, result) => {
      if (err) {
        reject(err)
      } else {
        resolve(result[name])
      }
    })
  })
}

const lockify = (f) => {
  var lock = Promise.resolve()
  return (...params) => {
    const result = lock.then(() => f(...params))
    lock = result.catch(() => {})
    return result
  }
}
const safePrompt = lockify(promisePrompt)

const fields = ['first', 'middle', 'last']

Promise
  .all(fields.map(safePrompt))
  .then(console.log.bind(console))
  .catch(console.error.bind(console))
$ node index.js prompt: first:  Neal prompt: middle:  Danger prompt: last:  Lindsay [ 'Neal', 'Danger', 'Lindsay' ] $

What does this mean?

It’s very rare that you have to worry about resource contention this way. Most of the asynchronous calls you make are also perfectly happy to run concurrently (like HTTP requests and file reads).

So then what can we take away from this exercise? First, promises are abstract things that can benefit from being thought about in a very “math-y” way, similar to how you think about other data types. You might have date utility functions that tell you when next Tuesday is, or whether a date is on a leap year. Your string utility library might have functions to capitalize words or normalize whitespace. Promises can also be manipulated in standard ways because of how we know they work, regardless of their enclosed values. Our lockify function doesn’t care what f() does, other than that it is a function that could return a promise.

Second, when you’re writing a function to explicitly manipulate promises, it’s important to keep in mind what happens immediately and what happens later. lock gets immediately set when we call lockify, and gets overwritten immediately on each call of the returned function. f, on the other hand, gets called after the previous lock resolves.

Third, at all other times it is important to forget about all that temporal stuff. The beauty of promises is that most of your code doesn’t worry about when it will run. It just says “when we have the data, do this stuff”. I love mapping across some data with a promise-returning function and then throwing the result into Promise.all. When does that stuff get done? Most of the time we don’t need to think about it.

Related Insights

🔗
Common patterns using JavaScript promises

Explore our insights

See all insights
Leadership
Leadership
Leadership
The business of AI: Solve real problems for real people

After participating in the Perplexity AI Business Fellowship, one thing became clear: the AI hype cycle is missing the business fundamentals. Here are 3 evidence-based insights from practitioners actually building or investing in AI solutions that solve real problems.

by
Cathy Colliver
Leadership
Leadership
Leadership
Pragmatic approaches to agentic coding for engineering leaders

Discover essential practices for AI agentic coding to enhance your team’s AI development learning and adoption, while avoiding common pitfalls of vibe coding.

by
A.J. Hekman
by
Aaron Gough
by
Alex Martin
by
Dave Mosher
by
David Lewis
Developers
Developers
Developers
16 things software developers believe, per a Justin Searls survey

Ruby on Rails developer Justin Searls made a personality quiz, and more than 7,000 software developers filled it out. Here's what it revealed.

by
Justin Searls
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.