Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toResolvedValue throwing incorrect error: "Unexpected asyncronous service when resolving service" #1759

Closed
2 of 4 tasks
holotrek opened this issue Mar 20, 2025 · 5 comments
Closed
2 of 4 tasks

Comments

@holotrek
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

Using .toResolvedValue to attempt to resolve a promise into the expected response results in an error "Unexpected asyncronous service when resolving service" despite the fact that it is not resolving the promise.

See this code for a minimal reproducible example.

Steps to reproduce

  1. Clone this repo: https://github.com/holotrek/react-native-inversify-example
  2. npm i
  3. npm run web
  4. Receive this error:
InversifyContainerError: Unexpected asyncronous service when resolving service "Symbol(CurrentCombatant)"
    at Container.get (/Users/evankaiser/code/react-native-inversify-example/node_modules/@inversifyjs/container/lib/cjs/container/services/Container.js:53:19)
    at RunTest (/Users/evankaiser/code/react-native-inversify-example/inversify.config.ts:85:35) {
  kind: 0,
  [Symbol(@inversifyjs/container/InversifyContainerError)]: true
}

Expected behavior

As stated here, .toResolvedValue is supposed to "wait for the promise to resolve before returning the resolved value to their dependent services". The dependency passed in bound to the symbol GET_COMBATANT_CHOICE_TYPE is the type Promise<Combatant>, but this language implies that it should automatically resolve to the parameter of type Combatant for the binding to CURRENT_COMBATANT_TYPE. Therefore no promise should remain.

Possible solution

No response

Package version

7.1.0

Node.js version

18.20.4

In which operating systems have you tested?

  • macOS
  • Windows
  • Linux

Stack trace

No response

Other

No response

@notaphplover
Copy link
Member

Hey @holotrek,

As you corretclty point container.toResolvedValue receives a Combatant, but the error is telling you're trying to get an async service using container.get. Consider using container.getAsync instead. This fixes your issue:

const expectedNinja = await container.getAsync<Combatant>(
  CURRENT_COMBATANT_TYPE
);

// ...

const expectedSamurai = await container.getAsync<Combatant>(
  CURRENT_COMBATANT_TYPE
);

I think I should update that section to indicate this. I'm closing the issue now, but if you think there's anything else to discuss or the issue should be reopen, feel free to ping me 🙂

@holotrek
Copy link
Author

holotrek commented Mar 20, 2025

@notaphplover Hmm, ok thanks...

I guess then I don't really understand the point of waiting for the promise to be resolved before injecting it into the other .toResolvedValue func. On the example page, it is implied that katanaDbCollectionSymbol is bound to AwesomeDbDriverCollection<Katana>, not Promise<AwesomeDbDriverCollection<Katana>>. I thought the whole point of waiting for the promise to be resolved is so you could bind to a non-PromiseLike and not have to await the call to getting it? Otherwise, there's no point, and I could simply bind to Promise<MyService> if I have to use container.getAsync anyway.

Essentially this is a little more code, but is much clearer as to what is actually happening:

container
  .bind(katanaDbCollectionSymbol)
  .toResolvedValue(
    (
      connection: Promise<AwesomeDbDriverConnection>,
    ): Promise<AwesomeDbDriverCollection<Katana>> => async {
      return (await connection).getCollection(Katana);
    },
    [dbConnectionSymbol],
  )
  .inSingletonScope();

const collection = container.getAsync<AwesomeDbDriverCollection<Katana>>(katanaDbCollectionSymbol);

@notaphplover
Copy link
Member

Hey @holotrek, you should not await for a promise since toResolveValue receives none:

container
  .bind(katanaDbCollectionSymbol)
  .toResolvedValue(
    (
      connection: AwesomeDbDriverConnection,
    ): AwesomeDbDriverCollection<Katana> => async {
      return connection.getCollection(Katana);
    },
    [dbConnectionSymbol],
  )
  .inSingletonScope();

const collection = await container.getAsync<AwesomeDbDriverCollection<Katana>>(katanaDbCollectionSymbol);

connection is not a Promise<AwesomeDbDriverConnection>, but AwesomeDbDriverConnection as docs states.

You are required to await the container.getAsync because it returns a Promise.

I hope it makes sense now 🙂

@holotrek
Copy link
Author

@notaphplover No, I understand how it works. My example is describing what would be more clear if you did NOT resolve the parameter passed to .toResolveValue. My point is that it should not bind to a promise if the function passed to .toResolvedValue does not return a promise. It makes no sense to pass the resolved value into the function if it is just going to turn it back into a promise in the end anyway.

@notaphplover
Copy link
Member

notaphplover commented Mar 20, 2025

@notaphplover No, I understand how it works. My example is describing what would be more clear if you did NOT resolve the parameter passed to .toResolveValue.

I see. Ok, then I'll try to explain the rationale behind this behavior.

Long story short: this behavior is the intended one in order to provide a good developer experience while providing a consistent behavior.

Why do we provide resolved value params instead of promises?

Well, let's say we go your way:

container
  .bind(katanaDbCollectionSymbol)
  .toResolvedValue(
    (
      connection: Promise<AwesomeDbDriverConnection>,
    ): Promise<AwesomeDbDriverCollection<Katana>> => async {
      return (await connection).getCollection(Katana);
    },
    [dbConnectionSymbol],
  )
  .inSingletonScope();

It seems clear connection is a Promise in this case, but the connection would be a promise if it's either an async value or it has async dependencies. It wouldn't be trivial at all to know whether or not a param is a promise, something that would lead us to type every expected T param as T | Promise<T>. Even if we manage to know T is not a promise, adding an async dependency would lead that param to be async, which would be frustrating to deal with in any non small codebase.

But the main reason is that the core engine does not only work with resolved values. A resolved value can be an async function, but a class constructor cannot. Providing promise values in a class constructor would be painful in lots of use cases since await is not allowed in a constructor. Providing a consistent behavior in both class and resolved values resolution encourage us to implement the feature as it currently is.

I hope I made myself clear now 🙂.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants