Best practices for dealing with Promise based APIs?
See original GitHub issueI’m having a bit of trouble finding an ergonomic pattern for dealing with a Promise based API. In particular I’m working with the google firestore API where I need to do transactions that follow this format:
let res: Promise<...> = firestore.runTransaction(async (transaction: Transaction) => {
.... potentially a bunch of code needed in this fn
}
So I reach for the fromPromise
API, but if I want to return a result from my transaction I end up with the following type
const res = ResultAsync.fromPromise(firesture.runTransction(async (transaction: Transaction) => {
....do some stuff
return err("invalid data")
.... do more stuff
return ok(someData)
}, (err) => new FirestoreTransactionError(err))
ResultAsync<Err<unknown, string> | Ok<myDataType, unknown>, FirestoreTransactionError>
. The best way I can see to unwrap the nested result is to chain a call to andThen
with an identity function.
const res = ResultAsync.fromPromise(...firestore transaction)
.andThen(v => v)
.andThen((someData) => {
....do stuff with result of successful transaction
})
It’s not a big deal to unwrap it with that identity function but it feels like I must be missing a better way. Is there a better way to deal with APIs like this that insist on taking an async fn for an argument and returning a promise?
It gets even more difficult to deal with since the firestore transaction API requires you to call methods on the transaction API it passes to your async fn that return even more promises.
As a side note, would it be possible to infer Result<T,U>
instead of Err<unknown, U> | Ok<T, unknown>
without specifying T and U? Sometimes I have really really long types (mobx-state-tree) that are annoying to specify explicitly. I guess it doesn’t really matter other than for easier to read types since I think those types are equivalent.
Issue Analytics
- State:
- Created 2 years ago
- Comments:6 (2 by maintainers)
Top GitHub Comments
Here’s what I ended up doing:
Then I also made my data load/save functions take an optional transaction and return ResultAsync so I can use it like so:
This way I can compose nice chains of Results and be able to handle the many possible error states. Before using never throw it was kinda impossible to figure out what errors might happen, it’s great! I uncovered a lot of potential bugs and incorrectness by refactoring to use results.
Just following up on this, I am looking for a pattern that calls async methods within a ResultAsync method…
The best I have come up with so far is as follows, however I feel the execute method gets a bit convoluted as it has to call connect first to ensure it’s connected and using the andThen pattern (vs what would have been an await this.connect if the method was async execute(query:string): Promise<Result<DriverRecordSetItems,>> ) feels like it’s starting to get messy.
Open to any other suggestions around how to structure something like this ?
Calling check or execute on the class is ideal though as the error handling is consistent and much nicer than the try/catch or other patterns!