Executing an asynchronous function within a TypeScript decorator

Apologies in advance for the lengthy question. I am striving to provide a clear explanation of the issue I am currently encountering.

During the development of my decorators utilities library, I came across an unexpected behavior while working on one specific decorator (https://github.com/vlio20/utils-decorators/blob/master/src/after/after.ts).

The decorator is named "after," and its purpose is to invoke a different function after the decorated method's execution. However, I noticed that if the function returns a promise, the decorator should wait for the promise to resolve before calling the after function.

Here is relevant code snippet:

        if (resolvedConfig.wait) {
          const response = await originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });
        } else {
          const response = originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });
        }

In the code above, I am using a flag to indicate whether the decorated method is asynchronous and returns a Promise. Ideally, I would like to eliminate the need for this flag by restructuring the code as follows:

        const response = await originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });

Essentially, I aim to always use the await keyword before executing the original method, under the understanding that it doesn't affect synchronous methods.

The issue arises when I make this modification as suggested above; a particular unit test fails:

  it('should verify after method invocation when method is provided', () => {
    let counter = 0;

    const afterFunc = jest.fn(() => {
      expect(counter).toBe(1);
    });

    class T {

      @after<T, void>({
        func: afterFunc
      })
      foo(x: number): void {
        return this.goo(x);
      }

      goo(x: number): void {
        expect(counter++).toBe(0);

        return;
      }
    }

    const t = new T();
    const spyGoo = jest.spyOn(T.prototype, 'goo');

    t.foo(1);
    expect(spyGoo).toBeCalledTimes(1);
    expect(spyGoo).toBeCalledWith(1);
    expect(afterFunc.mock.calls.length).toBe(1); // this line fails
  });

I have created a fork of the library where this specific test case is failing (https://github.com/vlio20/utils-decorators/pull/new/after-issue).

I'm puzzled about where my understanding might be incorrect. Any insights would be greatly appreciated.

Answer №1

In my approach, I always ensure to include await before executing the original method. Some may believe that in the case of a synchronous method, using await is redundant, but this notion is incorrect.

Contrary to popular belief, the AsyncFunction reference on MDN (which cites the ECMAScript spec) clarifies that any function labeled with async will consistently execute its function body outside of the regular call sequence.

To put simply, an async/await function operates independently of the caller and will always resolve asynchronously. It is crucial for functions to be either entirely synchronous or asynchronous, as stated in the return type of async functions, which invariably produces a promise no matter the internal operations. Promises cannot be synchronously inspected.

If you intend to enforce this behavior, it is recommended to forego employing await/async and assess the return type of your function directly:

const after = ({ func }) => (f) => (..args) => {
  const value = f(...args)
  if ('then' in value === false) {
    func()
    return value 
  }

  return value.then(value => {
    func()
    return value
  })
}

Based on the informative content provided, I advocate avoiding such practices. Consistently maintaining functions as either fully synchronous or asynchronous should be prioritized.

Answer №2

Perhaps this snippet of code can be of assistance to you:

const x = async () => console.log(await 'x')
x()
console.log('y')

The output will display y before x. This is because when using await, the async function will always execute after a short delay, requiring you to wait for it. That explains why all your synchronous functions in the test ran smoothly, while the asynchronous one did not.

If you were to include await with t.foo(1), the test would successfully pass.

In my view, it's advisable to have distinct implementations of afterFunc, as the presence of asynchronicity can be discerned from function.name.

For instance, consider the following scenario related to the decorator issue:

let isDecoratorDone = false

const decorator = (fn) => {
  return async (...args) => {
    await fn()
    isDecoratorDone = true
  }
}

const example = () => {
  let fnExecuted = false
  const fn = decorator(() => fnExecuted = true)
  fn()
  console.log(fnExecuted) // true
  console.log(isDecoratorDone) // you guess =)
}
example()

Once again, the remedy involves utilizing await within the test or creating both synchronous and asynchronous decorator implementations. For illustration purposes (apologies for not providing TypeScript):

const postFunction = function(fn, postFn) {
  // A package like is-async-function can be used here
  if (isFunctionAsync(fn))
    return (...args) =>
      new Promise(async (resolve, reject) => {
        try {
          const result = await fn.apply(this, args)
          await postFn() // Unclear if waiting for postFn is necessary
          resolve(result)
        } catch (err) {
          reject(err)
        }
      })
  else
    return (...args) => {
      const result = fn.apply(this, args)
      postFn() // Could be asynchronous; unsure about waiting for it
      return result
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The attribute 'size' is not recognized within the data type 'string[]' (error code ts2339)

When using my Windows machine with VSCode, React/NextJS, and Typescript, a cat unexpectedly hopped onto my laptop. Once the cat left, I encountered a strange issue with my Typescript code which was throwing errors related to array methods. Below is the co ...

Obtain item from JSON data structure

After querying my database, I receive a JSON object with the following structure: items = [ {"ID":1, "CODE":"code1", "DESCRIPTION":"abcd", "PRICE":100}, {"ID":2, "CODE":"code2", "DESCRIPTION":"efgh", "PRICE":100}, {"ID":3, "CODE":"code3", "DES ...

Install Chakra UI in the latest Next.js project directory

Recently, I decided to incorporate Chakra UI v2.4.9 into my Next.js project running on version v13.1.6. To ensure a seamless integration, I followed the detailed instructions provided in Chakra UI's official guide for Next.js. However, I encountered s ...

Tips for using NodeJS with a MySQL database

Hello everyone, I'm new to this community so please bear with me if my question seems too simplistic. I've been tasked with a basic web project for one of my courses and will be using NodeJS+MySQL along with VS Code which has caught my eye. Howe ...

What is the significance of utilizing response.json() for accessing JSON objects on both the server and client sides?

Just starting out with the MEAN stack, I've included my API code below where I'm using res.json(random) to send a random password. module.exports.changePass = function (req, res) { console.log(req.body.email) db.user.find({ where: { name: ...

Issue with looping in Javascript with jQuery

Can anyone explain why this for loop returns "bob" but "undefined" for "jacob"? $(function(){ count = 1; for(x=0; x <= count; x++) { track = $('#track' + x).val(); document.write(track ...

Progressive JQuery Ajax Requests

I'm attempting to retrieve specific information from a webpage, such as the title of the page and the domain name, and then perform an asynchronous POST request using jQuery with that extracted data. However, I've encountered an issue where my Ja ...

Utilize a randomized dynamic base URL without a set pattern to display a variety of pages

I am intrigued by the idea of creating a dynamic route that can respond to user-generated requests with specific files, similar to how Github handles URLs such as www.github.com/username or www.github.com/project. These URLs do not follow a set pattern, ma ...

Generating a dynamic triangle within a div while ensuring it fits perfectly with the maximum width

I need help with creating a responsive triangle on a web page that takes three inputs. The challenge is to adjust the size of the triangle so it fits inside a div element while maintaining the aspect ratio of the inputs. For instance, if the inputs are 500 ...

Identify numbers and words within a sentence and store them in an array

Looking to split a string into an array based on type, extracting numbers and floats. The current code is able to extract some values but not complete. var arr = "this is a string 5.86 x10‘9/l 1.90 7.00" .match(/\d+\.\d+|\d+&bsol ...

Obtain the updated dimensions of a Gridster widget following a resize operation

With the help of Gridster, a grid has been crafted containing resizable widgets through the configuration option resize.enabled. When a user completes resizing a Gridster widget, our goal is to retrieve the updated final size of the widget. What would be ...

Unlock the Power of Angular: Leveraging ViewEncapsulation.Native to Access HTML Elements

I am encountering an issue where I am receiving an error when trying to access an HTML element by ID. The problem arises when I attempt to access the classList upon a user clicking a button to apply a different style class to the element. The class list an ...

Stop Browsers from Saving the Current Page in the History Log

Currently, I am facing an issue with a user-triggered popup window on my website that redirects to another site. It is important that users do not access this page directly and only navigate to it if the popup window opens as intended. mysite.com -> my ...

verifying if checkbox is selected using a while loop in PHP

Help Needed: I am currently trying to loop through some code, but I'm struggling with checking checkboxes using PHP. Could someone please review my code and provide guidance on what needs to be added? Any assistance would be greatly appreciated. Thank ...

PhoneGap switches up the error type with each consecutive run

Why does PhoneGap change errors after every time it is compiled? Sometimes it runs without any issues, but then the same code throws strange errors like parse error or function not found, even though no changes were made to the code. Here is the code that ...

Switching the ng-class in Angular with a click event

I'm having trouble modifying a class I've created based on a value from JSON, and then changing it when the user clicks on it. Here's my code in the controller: $scope.like = function(){ if ($scope.class === "ion-ios-heart-outline") ...

Troubleshooting problem with AngularJS ng-repeat

Seeking assistance with an angularjs issue as a newcomer to the platform. Building a photo gallery where the initial page displays thumbnail photos from twitter and instagram using ng-repeat loop. Hovering over an image fades it out, revealing a button f ...

AngularJS controller encounters a scoping problem within a callback function

I have created an angular application with a simple login form that can be viewed on this JSFiddle. HTML Code: <form data-ng-app="jsApDemo" data-ng-controller="loginCtrl"> <label for="username">Username:</label> <input type=" ...

Deactivating the drag feature when setting the duration of a new event in FullCalendar

Hello there! I've integrated full calendar into my Angular project and I'm facing a challenge. I want to restrict users from defining the duration of an event by holding click on an empty schedule in the weekly calendar, where each date interval ...

Using ngIf for various types of keys within a JavaScript array

concerts: [ { key: "field_concerts_time", lbl: "Date" }, { key: ["field_concert_fromtime", "field_concert_totime"], lbl: "Time", concat: "to" }, { key: "field_concerts_agereq", lbl: "Age R ...