question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Best practices for dealing with Promise based APIs?

See original GitHub issue

I’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:closed
  • Created 2 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
evelantcommented, Feb 2, 2022

Here’s what I ended up doing:

export function withTransaction<T, U>(
    fn: ({ trans }: { trans: Transaction }) => ResultAsync<T, U>
): ResultAsync<T, U | FirestoreTransactionFailedErr> {
    return ResultAsync.fromPromise(
        firestoreInstance.runTransaction(async (transaction) => {
            // transaction.getRes = transGet.bind(undefined, transaction)
            return fn({ trans: transaction })
        }),
        (err: any) => new FirestoreTransactionFailedErr({ log, err })
    ).andThen((v) => v)
}

Then I also made my data load/save functions take an optional transaction and return ResultAsync so I can use it like so:

const res: ResultAsync<void, 
                  FirestoreTransactionError |
                  FooError | 
                  FirestoreNotFoundError | 
                  FirestoreNotAuthorizedError> =
 withTransaction(({trans}) => 
     getDocById(some_id, "some_collection", trans).andThen(my_doc => {
            if(my_doc.foo) {
               return errAsync(new FooError())
           } else {
               return saveDoc(my_doc, trans)
           }               
     })
    .andThen(.... and so on)
)

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.

0reactions
mikeburghcommented, Oct 11, 2022

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 ?

export class DB {

	private pool: any
	private connection:any //details omitted for brevity

	private connect(): ResultAsync<null, Error> {
		if (this.connection.connected) {
			return okAsync(null);
		}

		return ResultAsync.fromPromise(
			this.connection.connect(),
			(e: any) => e
		).andThen((pool) => {
			this.pool = pool;
			return okAsync(null);
		});
	}

	/* Execute a string of SQL! */
	execute(query: string): ResultAsync<RecordSetItems, Error> {
		
		return this.connect().andThen(() => {
			return ResultAsync.fromPromise(
				this.pool.query(query),
				(e: any) => e
			).andThen((results) => {
				return ok(
					results.recordsets.map((recordset: any) => {
						return {
							columns: recordset.columns,
							rows: recordset.map((row: any) => row),
						};
					})
				);
			});
		});

	}

	// Check our connection, so run empty query to check it works!
	check(): ResultAsync<null, Error> {
		return this.execute('SELECT 1').andThen((_) => ok(null));
	}
}

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!

const result = await instanceOfDb.check(); 
if (result.isErr()) {
....
}

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to implement a promise-based API - MDN Web Docs
Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals, including event handling and the basics of ...
Read more >
Best Practices for ES6 Promises - DEV Community ‍ ‍
Best Practices for ES6 Promises · Handle promise rejections · Keep it "linear" · util.promisify is your best friend · Avoid the sequential...
Read more >
Promise API - The Modern JavaScript Tutorial
There are 6 static methods in the Promise class. We'll quickly cover their use cases here. Promise.all. Let's say we want many promises...
Read more >
A beginners guide to Promise API in Javascript - Medium
A Promise guarantees the execution of 'resolve' or 'reject' methods only after the work is done. This is done by resolving the Promise...
Read more >
Using Promises With REST APIs in Node.js - Twilio
Learn how to use JavaScript Promises and the async and await keywords to perform a series of related REST API calls with this...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found