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

add unwrap() method to supabase client #92

Closed
andykais opened this issue Dec 8, 2020 · 16 comments
Closed

add unwrap() method to supabase client #92

andykais opened this issue Dec 8, 2020 · 16 comments
Labels
enhancement New feature or request

Comments

@andykais
Copy link

andykais commented Dec 8, 2020

Feature request

add an unwrap() method or a an either monad left() method to pull the data out of a postgrest response.

Is your feature request related to a problem? Please describe.

Not exactly a problem, but the way the client is designed requires every database query to individually handle database failures. For the most part, I want to assume that the query succeeded, and throw an error if it did not. That makes most of my db calls look like this:

  const res = await context.supabase
    .from<tables['my_table']>('my_table')
    .select()
    .filter('status', 'eq', 'COMPLETED')
    .limit(1)
  if (res.error) throw new Error(`SupabaseError: query failed: \n${res.error}`)

Describe the solution you'd like

The solution I would like best, would be to add a method which can optionally unwrap the { error: object } | { data: object[] } response from supabase into just the data field. If there is an error present, the client should throw an error. This should not break any existing workflows, its just an optional convenience method.

Usage:

const data: table['my_table'] = await supabase.from<table['my_table']>('my_table')
  .select()
  .unwrap()

// handling an error:
try {
  const data = await supabase.from('my_table').delete().unwrap()
} catch (e) {
  if (e.name === 'SupabaseError') {
    // handle it
  } else {
    throw e
  }
}

Describe alternatives you've considered

I have attempted building this on top of the supabase client, but because of the way queries are built, I need to use proxies:

  const supabase_proxy_handler = {
    get: function (target: any, prop: string) {
      if (prop === 'unwrap') {
        return new Promise((resolve, reject) => {
          target.then((res: PostgrestResponse) => {
            if (res.error) throw new Error(`SupabaseError: query failed: \n${JSON.stringify(res.error)}`)
            else return res.data
          })
        })
      } else {
        return target[prop]
      }
    },
  }
  const supabase_proxy = new Proxy(supabase, supabase_proxy_handler)

This technically works, but manipulating the types to include the unwrap() method is harder.

Additional context

unwrap is a common pattern in several languages & libraries. Here is an example in rust: https://doc.rust-lang.org/rust-by-example/error/option_unwrap.html

@andykais andykais added the enhancement New feature or request label Dec 8, 2020
@emschwartz
Copy link

Out of curiosity, why do all the calls return an error instead of rejecting the Promise? This seems like a very unusual pattern for Javascript/Typescript.

@andykais
Copy link
Author

@emschwartz they are rejecting the promise. This is async/await syntax, which can use try/catch blocks to catch rejected promises

@emschwartz
Copy link

They are? I'm familiar with async/await syntax but all of the examples here show data and error in the object that the Promise resolves to:

const { data, error } = await supabase
  .from('cities')

As as result, it seems like we need to write code like this:

try {
  const { data, error } = await supabase
    .from('cities')
  if (error) {
    throw error
  }
} catch (err) {
  // handle err
}

If the Promise were rejected instead of returning the error object, you could leave out the if (error) { throw error } bit.

(This is the line I would expect to throw instead of returning)

@andykais
Copy link
Author

I understand your confusion now. If you look at my first message, the error is being thrown in the "Describe the solution you'd like" section. This is my suggestion on how an unwrap method should work. Above that I have an example of how the supabase client works currently, which is what you are describing.

@emschwartz
Copy link

Oh whoops, @andykais sorry for the misunderstanding. My question was actually about the library as it currently stands rather than your suggestion.

I think your proposal seems like a reasonable way to improve the ergonomics without breaking backwards compatibility.

If there is going to be a major version change with breaking changes, I would vote for the pattern to be changed entirely so that all errors cause the Promise to be rejected.

@kiwicopple
Copy link
Member

Hi @emschwartz - this is just a DX decision. We purposely chose to return errors rather than throw them. There is no right or wrong here, only preferences. We decided to return because (we believe) it is a nicer developer experience :) . I'm sorry you don't share the view - I didn't either until I started using it, so perhaps this library will change your mind

@emschwartz
Copy link

Hi @kiwicopple I hear that and could accept that it's just a different style choice if the Promise were guaranteed never to reject (because then you wouldn't need to wrap it in a try/catch). However, this call to fetch can "reject on network failure or if anything prevented the request from completing" (MDN), which means you need to handle the error in two places instead of one. Always wrapping it in a try/catch and re-throwing the error returned seems worse than handling all types of errors in one way.

All of that said, I'm using your open source library so take my comments with a grain of salt :)

@malobre
Copy link

malobre commented Feb 2, 2021

I agree with @emschwartz, we end up try-catch'ing + checking for error in the returned object.
If the goal was to enhance the DX I think it should be harmonized (e.g catch thrown errors and put them in res.error).

Edit: The auth part already does this (e.g signUp)

@Nick-Mazuk
Copy link

I do second this feature request.

Since a database call isn't usually the only thing that's happening, I've found that I've often needed to double catch errors. For instance:

try {
    const { data, error } = await supabase.from('table').select('*')
    if (error) throw new Error(error)

    // do other post-processing
} catch (error) {
    // handle the errors
}

This is a short example, but it can get especially cumbersome when combining this with other API calls for other services. Here's a contrived example that should hopefully get the point across.

try {
    const [{ data, error }, fetchResponse, otherApiResponse] = await Promise.all([
        supabase.from('table').select('*'),
        fetch('https://example.com/api-endpoint'),
        someOtherApiFunctionCall(),
    ])
    if (error) throw new Error(error)

    // do other post-processing
} catch (error) {
    // handle the errors
}

Notice how the Supabase error is handled differently from everything else?

Or what if you don't use async/await, and you use then/catch. For instance, inside of a react useEffect hook, then/catch is preferred since you cannot use async/await directly in hook.

const [posts, setPosts] = useState()
const [errorMessage, setErrorMessage] = useState('')

// example with no error handling
useEffect(() => {
    supabase
        .from('posts')
        .select('*')
        .then(({ data }) => setPosts(data))
}, [])

// with error handling
useEffect(() => {
    const getData = async () => {
        const { data, error } = await supabase.from('posts').select('*')
        if (error) setError(error)
        else setPosts(data)
    }
    getData()
}, [])

// error handling if supabase unwrapped promises
useEffect(() => {
    supabase
        .from('posts')
        .select('*')
        .unwrap()
        .then((data) => setPosts(data))
        .catch((error) => setError(error))
}, [])

I would argue that the unwrap option is the clearest option to read and write.

I understand that this is a matter of opinion and not objective fact, but when a lot of JavaScript is built around being able to reject promises, I find it hard to believe that not rejecting on errors is always better. At least having the option with unwrap I think is much better than the current schema.

@mstade
Copy link

mstade commented Jun 3, 2021

For what it's worth, the decision to return the error as a result rather than throw is described in #32. I opened a discussion about this some time ago asking pretty much the same thing as you, with a slightly (but not meaningfully) different take on it: supabase/supabase#604

We've found that we use both the { data, error } = supabase ... pattern and a variant of unwrap, so I'm not sure either way is strictly better and an .unwrap() modifier seems like a good way to go. What we do with our variant right now is (ironically) to wrap the query and throw if there's an error, like so:

try { 
  const data = await throwOnError(supabase ...)
} catch (error) {
  ...
}

I'm gonna take a stab at a PR for this, because it's annoying me to no end. 😅

@mstade
Copy link

mstade commented Jun 4, 2021

Proposed implementation available in supabase/postgrest-js#188.

soedirgo pushed a commit to supabase/postgrest-js that referenced this issue Jun 11, 2021
…hem (#188)

* Implement unwrap() to throw query errors instead of returning them

The rationale and motivation for this change is well described in supabase/supabase-js#92,
and further supported by the discussion in supabase/supabase#604.

This implementation doesn't actually unwrap the result as described in supabase/supabase-js#92,
i.e. returning only the actual data. This is done deliberately for two reasons:

1. Making sure the caller can still extract `count`, `status` and
`statusText` while still benefitting from errors being thrown instead of
returned
2. Ease the transition from the non-throwing pattern where destructuring is
norm anyway, so less code has to be rewritten to take advantage of unwrap

Basic tests were added to test/basic.ts and the unwrap function is
documented to some degree at least, though this can probably be improved.

* Rename the unwrap API to throwOnError instead

The `unwrap` moniker was a bit of a misnomer, since it didn't actually
unwrap the result, and this should make things a bit more clear. This was
discussed in a bit more detail in #188.
@mstade
Copy link

mstade commented Jun 11, 2021

I guess supabase/postgrest-js#188 doesn't technically fit the definition of the unwrap pattern described here, but more or less solves the problem anyway? Is it enough to close this issue, @andykais?

@andykais
Copy link
Author

I would say it definitely covers my initial use case @mstade. Hats off to you! Thanks for putting in the time.

@mstade
Copy link

mstade commented Jun 12, 2021

Happy to help! 😊

@riderx
Copy link

riderx commented Feb 17, 2023

OMG i just discovered that now, felt so annoyed by that as well, i tried to unwrap in SDK v2, but method seems not available how to use it in V2?

@riderx
Copy link

riderx commented Feb 17, 2023

Ok found it .throwOnError()

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

No branches or pull requests

7 participants