Skip to content

refactor: simplify callback return types to void #9245

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

Conversation

braden-w
Copy link
Contributor

@braden-w braden-w commented Jun 3, 2025

This PR simplifies the callback return types from Promise<void> | void to just void. This is a more idiomatic TypeScript approach that maintains the same functionality while being more concise, and addresses user feedback from #9202.

PR #9202 narrowed callback types from Promise<unknown> | unknown to Promise<void> | void to better reflect the intent that callback return values are ignored. However, this change broke existing user code that was using a common pattern of returning Promise.all() from callbacks.

## Problem

The change broke code like this:

const useCreateProduct = () => {
  return useMutation({
    mutationFn: (createProductDto) => api.products.create(createProductDto),
    onSuccess: (data) => 
      Promise.all([
        queryClient.invalidateQueries({ queryKey: ['products'] }),
        queryClient.invalidateQueries({ queryKey: ['categories'] })
      ]),
  });
};

Since Promise.all() returns Promise<Array<void>>, this pattern was no longer type-compatible.

## Solution
In TypeScript, when a function type is specified as void, it can accept both synchronous functions that return void and asynchronous functions that return Promise<void>. This is because TypeScript's type system is designed to be more permissive with void return types in function signatures.

This PR removes Promise<void> from the union type and turns it to just void, allowing the common Promise.all() pattern while maintaining the intent that individual callback return values are ignored (they're all variations of void).

The previous PR (#9241) attempted to handle Promise returns explicitly, which led to more complex code and type definitions. This PR takes a simpler approach by:
1. Using TypeScript's built-in void type which already handles both sync and async cases
2. Removing unnecessary Promise chaining
3. Making the code more maintainable and easier to understand

## Addresses

- Feedback from @Sagie501 in #9202
- Feedback from @jgarplind in #9202
- Common Promise.all() usage pattern in mutation callbacks

Fixes #9202 (user feedback)

braden-w added 7 commits May 28, 2025 00:03
…to Promise<void> | void

Previously, the `onSuccess`, `onError`, `onMutate`, and `onSettled` callbacks were typed as `() => Promise<unknown> | unknown`. While `unknown` is technically valid, it implies that the return value might be used, assigned, or further processed. However, throughout the codebase, these callbacks are invoked solely for their side effects, and their return values are always ignored. Narrowing the type to `Promise<void> | void` makes this intent explicit, clarifies that any return value will be discarded, and prevents misleading type signatures that suggest otherwise.

This commit narrows their types to `() => Promise<void> | void`, which more accurately reflects their intended use.
…n-related documentation

This commit refines the documentation for mutation-related callbacks (`onSuccess`, `onError`, `onSettled`, and `onMutate`), changing their return types from `Promise<unknown> | unknown` to `Promise<void> | void`.
…e<Array<void>> for improved handling in onSuccess and onError
Copy link

nx-cloud bot commented Jun 3, 2025

View your CI Pipeline Execution ↗ for commit 77b0cab.

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 1m 20s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2025-06-05 00:18:50 UTC

Copy link

codecov bot commented Jun 3, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 59.64%. Comparing base (5e3cd46) to head (d16c02c).
Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9245       +/-   ##
===========================================
+ Coverage   45.33%   59.64%   +14.30%     
===========================================
  Files         207      136       -71     
  Lines        8271     5509     -2762     
  Branches     1865     1480      -385     
===========================================
- Hits         3750     3286      -464     
+ Misses       4080     1925     -2155     
+ Partials      441      298      -143     
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental 85.00% <ø> (ø)
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister 43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental 24.39% <ø> (ø)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core 98.15% <ø> (ø)
@tanstack/query-devtools 3.55% <ø> (ø)
@tanstack/query-persist-client-core 78.32% <ø> (ø)
@tanstack/query-sync-storage-persister 84.61% <ø> (ø)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query 96.02% <ø> (ø)
@tanstack/react-query-devtools 10.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 78.13% <ø> (ø)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query 88.07% <ø> (ø)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client 100.00% <ø> (ø)
@tanstack/vue-query 70.85% <ø> (ø)
@tanstack/vue-query-devtools ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@braden-w
Copy link
Contributor Author

braden-w commented Jun 3, 2025

@rothsandro That's a great point.

I was playing around with different return signatures. Unfortunately, even if we wanted to accommodate the eslint error and widened the type to be void | Promise<void> | Promise<Array<void>>, this ends up causing more type errors whenever the callback function is synchronous and returns a value other than void.

Consider the query3 example in this code snippet:

https://typescript-eslint.io/play/#ts=5.8.2&fileType=.tsx&code=FASwdgLgpgTgZgQwMZQAQDkExgewO4CKArrAJ6oDewqqOYAykUigM4sBcqAFAJSoC8APlQA3HCAAmAbmABfYMCR0WEVOBEIANpITQB3PkNQAFXAFsQLKADoYUFjk0iovGYuWqAjiRilOmbHxiMn0qGjpGZnsOAwFhUxwLK2stTS4AbXUtHWheABo1MA1tCV0XHgBdHjy5NwVwaHhkNAB1SShg30pqWgYmVhjeONFxCVQAHxNzSygAHjFJYUmEpLmAQWwEUnnRwUEZeXcwFVRvMgAmTjaJDp9yfm7wvqi2TiGjFZmUzTTMouzSrlqoVijlylUarI3EpjqoIDgECcHu9hGFUDCTpJ9ABZXQAC1sCDAEkSvB6GMcNk0OAA5lwAESmKDQYmoeGIiD0ng9OwQIgwMBqCRyBQYrx3ADMV3anXuj16kQGb0MwnZKnytQUQA&eslintrc=N4KABGBEBOCuA2BTAzpAXGUEKQAIBcBPABxQGNoBLY-AWhXkoDt8B6Jge1oFtLlZkiACa1i0Dr0GoMkRNHHRI4MAF8QKoA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false

For some reason, it seems like the Typescript compiler can't accept a function that returns a number, even though it was just fine earlier when return types of callback functions were just void.

Since it seems like your example works under our current eslint config (we don't have any no-misused-promises in query's eslint config), I would like to propose that void still might be our best option. That is the simplest return type that passes all of our requirements, while conveying that the return values of callbacks are discarded. How does that sound?

@rothsandro
Copy link

Isn't just void even more confusing than the original Promise<unknown> | unknown type? Because if I return a promise it's awaited and I wouldn't expect that if just void is accepted.

@braden-w
Copy link
Contributor Author

braden-w commented Jun 3, 2025

Good point, using just void is not a good idea, especially since we do await onSuccess and onError if they return Promises:

https://tanstack.com/query/v5/docs/framework/react/guides/mutations#mutation-side-effects

The issue with Promise<unknown> | unknown is explained in #9202 and primarily readability: the actual return types of these functions is discarded after it's called, but it's not immediately obvious with unknown. We ended up resolving on Promise<void> | void, but it was struggling to handle onSuccess: () => Promise.all(...). We also have a precedent of turning callback return types to void, as suggested here: #4133.

It gets confusing because it seems like we are awaiting onSuccess in mutation.ts but not awaiting it in mutationObserver.ts.

In summary, Promise<void> | void struggles with handling onSuccess: () => Promise.all(...), and Promise<unknown> | unknown has been replaced before with Promise<void> | void or void.

@TkDodo , what's the call here? Seems like we have several options:

  1. Revert refactor: narrow onSuccess/onError/onMutate/onSettled callback types to Promise<void> | void #9202 to use Promise<unknown> | unknown, but we have some disconnect since previous PRs like Docs: update mutate options description #4133 seem to prefer void over unknown if the return type is discarded
  2. Merge this in to use void instead of Promise<void> | void, fixing the onSuccess: () => Promise.all(...) issue, but consumers have to know about how async onSuccess/onError will be awaited before calling onSettled without it being implied in the type signature.
  3. Keep refactor: narrow onSuccess/onError/onMutate/onSettled callback types to Promise<void> | void #9202 but make it very clear that onSuccess/onError functions shouldn't explicitly return anything. So for example, people should refactor onSuccess: () => Promise.all(...) to onSuccess: () => { Promise.all(...) }, as mentioned here.
  4. Or maybe there is a misunderstanding, since we await callbacks in mutation.ts but not in in mutationObserver.ts. Is there a reason we await in one and not in the other.

@TkDodo
Copy link
Collaborator

TkDodo commented Jun 3, 2025

@braden-w I think we should revert back to Promise<unknown> | unknown. Even though we don’t use the return value, it’s important that users know that they can return a Promise, which will be awaited by us.

we don’t await on the observer callbacks because they run after the cache entry has been written, for each observer, so they are fire-and-forget. Returning a promise from the onSuccess callback of the cache keeps the mutation in pending state until that promise has resolved. We can’t do that for observer callbacks.

@Ephem
Copy link
Collaborator

Ephem commented Jun 4, 2025

I have not been following this closely, but could typing it as Promise<void> | Promise<Array<void>> | void be an option?

@TkDodo
Copy link
Collaborator

TkDodo commented Jun 4, 2025

I have not been following this closely, but could typing it as Promise<void> | Promise<Array<void>> | void be an option?

yeah if Promise.all is the only issue, we could make a type out of Promise<void> | Promise<Array<void>> | void and use that everywhere.

it might have to be Promise<ReadonlyArray<void>> in case someone uses const assertions?

in any case, some type-level tests for these things would be great so that we don’t regress again 🙏

@jjmean2
Copy link
Contributor

jjmean2 commented Jun 4, 2025

What if we use Promise.allSettled()? This often comes up when we want to trigger multiple async effects on query success, but we don't actually care whether each one succeeds or fails, we just want to wait for all of them to complete.

If we consider this case, perhaps the return type should be:

void | Promise<void> | Promise<Array<void>> | Promise<Array<PromiseSettledResult<void>>>

This type feels a bit more verbose than just void or unknown | Promise<unknown>

Sometimes we want to reuse existing functions that return values, even though we don’t care about the return values in this context. For example:

onSuccess: (data) => Promise.all([
  updateSomeModel(data), // returns updated model, but we ignore it
  logSomeAction(data),   // returns true/false, but we ignore it
])

In this case, the return type doesn’t match Promise<Array<void>>, so we’d need to rewrite the code like this to satisfy the type checker:

onSuccess: async (data) => {
  await Promise.all([
    updateSomeModel(data),
    logSomeAction(data),
  ])
}

This works fine, but it feels a bit restrictive.

I think unknown | Promise<unknown> is a good choice. Technically, it’s equivalent to just unknown, so it allows any return value, similar to how void works.
And I think allowing any return value can convey that the return value doesn't really matter.
And by specifying Promise<unknown>, it can also convey that if a promise is returned, it will be awaited .

@TkDodo
Copy link
Collaborator

TkDodo commented Jun 4, 2025

How about void | Promise<unknown> ? I have seen some cases where users return something from onSuccess and expect this to do something. void would signal that this isn't the case.

e.g. this discord thread: https://discord.com/channels/719702312431386674/1379733266604494968

@braden-w
Copy link
Contributor Author

braden-w commented Jun 4, 2025

@TkDodo That would still unfortunately throw an error if we did:

const toast = () => {
  const id = Math.random()
  console.log("Pretend toast")
  return id
}

// throws error since onSuccess returns a sync non-void value
onSuccess: () => toast(),

But what if we did void | Promise<void> | unknown | Promise<unknown> (which could eventually be simplified to MaybePromise<void> | MaybePromise<unknown>? That would genuinely widen the type to fit everything while still carrying the void connotation.

I also wonder if this might be a genuine use case for any over unknown. So MaybePromise<void> | MaybePromise<any> or even just MaybePromise<any>:

  • We genuinely don't care about the type
  • The result is thrown away, so type safety provides no value
  • Users can write any expression without TypeScript complaints
  • We're saying "give us literally anything, we ignore it

In general, the the semantic difference feels like:

  • unknown: "We don't know what this is, so be careful"
  • any: "We don't care what this is, so do whatever"

Since we're discarding the return value entirely, any might be more semantically honest. We're not asking users to be careful with an unknown type - we're telling them the type doesn't matter at all. This might be one of the rare cases where any is not just acceptable, but actually the better choice than unknown.

So the final options could be:

  1. void | Promise<void> | unknown | Promise<unknown> aka MaybePromise<void> | MaybePromise<unknown>
  2. void | Promise<void> | any | Promise<any> aka MaybePromise<void> | MaybePromise<any>
  3. any | Promise<any> aka MaybePromise<any>

@TkDodo
Copy link
Collaborator

TkDodo commented Jun 4, 2025

Shouldn't the example be:

onSuccess: () => {
  toast()
}

i think I'd be fine with onSuccess: () => toast() erroring, but I don't have strong feelings

@jjmean2
Copy link
Contributor

jjmean2 commented Jun 4, 2025

@braden-w

I think unknown is a better choice than any in this context.

Using any allows you to do anything with the value, which makes it easy to use in an onSuccess callback without worrying about type errors. On the other hand, unknown restricts what you can do with the value, signaling to the caller that the value isn’t meant for any specific use.

While void would better represent this intention, it’s often too restrictive when used in a union with Promise. From the caller’s perspective, unknown is a more flexible alternative that still conveys the idea that the value isn’t intended to be used directly.

I actually considered writing it like this:

onSuccess?: 
  | ((data: TData, variables: TVariables, context: TContext) => void)
  | ((data: TData, variables: TVariables, context: TContext) => Promise<void>)
  | ((data: TData, variables: TVariables, context: TContext) => void | Promise<void>)

But it might be a bit too verbose.


If it’s important to inform that the return value—aside from a Promise—is not meant to be used, then I think using void | Promise<void> is a reasonable option. It may require a bit more boilerplate to work around type errors—for example, adding .then(noop) or using await instead of returning the Promise directly—but the change itself should be straightforward, so it’s a trade-off.

On the other hand, if enforcing this at compile time isn’t a high priority, using unknown | Promise<unknown> and simply documenting that the return value should be ignored could be a good alternative.

@braden-w
Copy link
Contributor Author

braden-w commented Jun 4, 2025

@jjmean2 That's a really cool solution! It seems like that would allow us to have the benefits of implying that the return value is discarded and async/sync while still allowing the onSuccess: (data) => Promise.all(...) edge case to still be accepted. Although maybe we could simplify it to

  | ((data: TData, variables: TVariables, context: TContext) => void)
  | ((data: TData, variables: TVariables, context: TContext) => Promise<void>)

This would remove previous false positive errors for consumers while still conveying the idea that the value isn’t intended to be used directly.

It may require a bit more boilerplate to work around type errors—for example, adding .then(noop) or using await instead of returning the Promise directly—but the change itself should be straightforward, so it’s a trade-off.

I don't think this would require too much extra boilerplate or a noop. Will work on a quick implementation to see!


i think I'd be fine with onSuccess: () => toast() erroring, but I don't have strong feelings

@TkDodo I agree that

onSuccess: () => {
  toast()
}

would pass through void | Promise<unknown> but I don't think onSuccess: () => toast() should error; the value is discarded but that doesn't mean that we should only accept functions that don't return a value; they should still be accepted. We just want to give users some more context that returned values are not consumed or used.

Also for some reason, onSuccess: () => toast() wouldn't error if we typed onSuccess as purely => void instead of any union => void | Promise<unknown>. The union in the return type breaks things, but @jjmean2 's solution would fix that perfectly by moving the union to be of the function type entirely rather than the return type.

At this point, I prefer @jjmean2 's solution, but can understand why we might want to go for unknown | Promise<unknown>.

Copy link

pkg-pr-new bot commented Jun 4, 2025

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9245

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9245

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9245

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9245

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9245

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9245

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9245

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9245

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9245

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9245

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9245

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9245

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9245

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9245

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9245

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9245

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9245

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9245

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9245

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9245

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9245

commit: 77b0cab

Replace callback return type unions
with unions of complete function signatures to improve type safety and clarity.

This change addresses the issues raised in TanStack#9241 and TanStack#9245 by:
- Preventing mixed sync/async behavior within single callbacks
- Maintaining support for Promise.all() patterns
@braden-w
Copy link
Contributor Author

braden-w commented Jun 5, 2025

Update: Just tried implementing it in this commit, and it works great in these cases without error:

// 1. Implicit void return
onSuccess: (data, variables, context) => {
  console.log('Success!')
}

// 2. Explicit void return
onSuccess: (data, variables, context) => {
  console.log('Success!')
  return
}

// 3. Explicit undefined return
onSuccess: (data, variables, context) => {
  console.log('Success!')
  return undefined
}

// 4. Async function with implicit void
onSuccess: async (data, variables, context) => {
  await saveToDatabase(data)
}

// 5. Async function with explicit void return
onSuccess: async (data, variables, context) => {
  await saveToDatabase(data)
  return
}

// 6. Promise.resolve() pattern
onSuccess: (data, variables, context) => {
  return Promise.resolve()
}

// 7. Promise.all() pattern (the original use case!)
onSuccess: (data, variables, context) => {
  return Promise.all([
    queryClient.invalidateQueries({ queryKey: ['products'] }),
    queryClient.invalidateQueries({ queryKey: ['categories'] })
  ])
}

// 8. Complex async operations
onSuccess: (data, variables, context) => {
  return Promise.all([
    analytics.track('product_created'),
    notifications.show('Success!'),
    queryClient.invalidateQueries({ queryKey: ['products'] })
  ]).then(() => {
    console.log('All operations completed')
  })
}

But from the commit diff, you can see it is quite verbose. Just hovering on a callback function honestly gives worse DX:

CleanShot 2025-06-04 at 17 01 19

And also, technically speaking, the function should be able to return a mix of both void and Promise<void> in the same function:

onSuccess: (data, variables, context) => {
  if (someCondition) {
    return // void
  } else {
    return Promise.resolve() // Promise<void>
  }
  // Error: Cannot mix sync and async returns in same function
}

This example would technically pass since the function matches to ((data: TData, variables: TVariables, context: TContext) => void), since void matches both void and Promise<void> as mentioned earlier. But at that point, it makes me question whether it's worth it to include | ((data: TData, variables: TVariables, context: TContext) => Promise<void>).

So in summary, I think unknown | Promise<unknown> is our best shot. In an ideal world, I would really prefer void | Promise<void>, and will miss it, but I don't think it's worth breaking support for all the onSuccess: (data) => Promise.all(...) edge cases and telling them all to do () => { Promise.all(...) } instead of () => Promise.all(...). unknown | Promise<unknown> is less precise, but it won't cause any existing users to have Typescript errors, and we can mitigate the downsides in our documentation.

**Update: Just made a PR to revert to Promise<unknown> | unknown in #9251 **

@braden-w braden-w closed this Jun 5, 2025
@braden-w braden-w reopened this Jun 5, 2025
@braden-w braden-w closed this Jun 5, 2025
braden-w added a commit to epicenterhq/query that referenced this pull request Jun 5, 2025
Adds test coverage for mutation callback return types, inspired by the discussions and [TkDodo's comment](TanStack#9245 (comment)
) in TanStack#9245 and the original Promise.all() edge case identified in TanStack#9202.

The original PR TanStack#9202 narrowed callback return types from `Promise<unknown> | unknown`
to `Promise<void> | void`, but this broke common patterns like:

```ts
onSuccess: (data) => Promise.all([
  invalidateQueries(),
  trackAnalytics(),
])
```

As noted in [the original discussion](TanStack#9202 (comment)),
this `Promise.all()` pattern was a legitimate use case that many users relied on.

These tests ensure we support all the callback patterns that users expect:

✅ **Sync patterns**: Implicit void, explicit void, non-void returns
✅ **Async patterns**: Async functions, Promise.resolve(), Promise<T> returns
✅ **Promise.all() patterns**: The original breaking case from TanStack#9202
✅ **Promise.allSettled() patterns**: Additional parallel operation support
✅ **Mixed patterns**: Different callback types in same mutation
✅ **Error handling**: All patterns work in error scenarios
✅ **Return value isolation**: Callback returns don't affect mutation result
TkDodo added a commit that referenced this pull request Jun 5, 2025
* Revert "refactor(types): narrow onSuccess/onError/onMutate/onSettled callback types to Promise<void> | void (#9202)"

This reverts commit 982f6ca.

* test: add callback return type tests for mutation callbacks

Adds test coverage for mutation callback return types, inspired by the discussions and [TkDodo's comment](#9245 (comment)
) in #9245 and the original Promise.all() edge case identified in #9202.

The original PR #9202 narrowed callback return types from `Promise<unknown> | unknown`
to `Promise<void> | void`, but this broke common patterns like:

```ts
onSuccess: (data) => Promise.all([
  invalidateQueries(),
  trackAnalytics(),
])
```

As noted in [the original discussion](#9202 (comment)),
this `Promise.all()` pattern was a legitimate use case that many users relied on.

These tests ensure we support all the callback patterns that users expect:

✅ **Sync patterns**: Implicit void, explicit void, non-void returns
✅ **Async patterns**: Async functions, Promise.resolve(), Promise<T> returns
✅ **Promise.all() patterns**: The original breaking case from #9202
✅ **Promise.allSettled() patterns**: Additional parallel operation support
✅ **Mixed patterns**: Different callback types in same mutation
✅ **Error handling**: All patterns work in error scenarios
✅ **Return value isolation**: Callback returns don't affect mutation result

* refactor(tests): remove unnecessary parameters in mutation tests

* test(mutations): fix mutation test ordering with error handling and cleanup verification

---------

Co-authored-by: Dominik Dorfmeister <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants