feat: add a method for returning an object
This will allow for handling errors or data using a single reference
Thanks for the PR! Originally I chose a tuple so the consumer can name both values easily, but I like the idea of allowing the return to be an object.
That being said, I'm not 100% sold on having another function with an ambiguous name, both trytm and try$ don't say what they do. It might be fine considering the library's size, but I'd like to get more input about it before going forward.
A questionable alternative I thought about while writing this was having a config object as the second parameter of the function, I kind of hate it but will throw it here for the sake of discussion.
const data = trytm(promise(), {return: "object"})
const [dataB, err] = trytm(promise(), {return: "tuple"});
const [dataB, err] = trytm(promise());
This would add a couple of ifs inside the function and ship unnecessary code to users that just want one function or the other so I don't really like it. Additionally, users wouldn't be able to choose one or the other by default so they'd be forced to pass the second argument every time if they want the object. Again, don't really like this option...
What about having trytm as a wrapper for both and one as a default?
// dist.ts
const trytm = {
tuple: functionThatReturnsTuple,
obj: functionThatReturnsObj
}
// userLand.ts
import { trytm, tuplePlaceholderName, objPlaceholderName}
const data = trytm.obj(promise);
const dataB = objPlaceholderName(promise);
const [dataC, errC]= trytm.tuple(promise);
const [dataD, errD] = tuplePlaceholderName(promise);
const [dataE, errE] = trytm(promise); // would default to tuple to maintain compatibility?
This feels better than the other one I wrote above but I'm not 100% sold on this either.
In the end, I think it comes down to:
- do we want more than one function in the lib?
- if yes, should their names make sense, or is
tmand similar fine? - if no, how do we go about surfacing multiple behaviors?
- if yes, should their names make sense, or is
Again, thanks for the PR and I hope we can take this in a direction that makes the lib better :)
Any feedback on how to go about this is appreciated
That being said, I'm not 100% sold on having another function with an ambiguous name, both trytm and try$ don't say what they do. It might be fine considering the library's size, but I'd like to get more input about it before going forward.
I totally agree with this, and part of the reason I implemented with this weird-ass name was to spark conversation about the exported methods' names.
Most of the time I implemented this exact pattern, I named the function handleError -- but I feel it's really common to name functions with this name, as there has been a couple times where I named it catchError.
My intuitive response would be call this catchError and make the return both a tuple and an object, by overriding the Symbol.iterator attribute:
export const trytm = async <T>(
promise: Promise<T>,
): Promise<TryResult<T>> => {
const getResult = (data: T | null, error: Error | null) => ({
data,
error,
[Symbol.iterator]: function* () {
yield this.data;
yield this.error;
}
})
try {
const data = await promise;
return getResult(data, null);
} catch (throwable) {
if (throwable instanceof Error) {
return getResult(null, throwable);
}
throw throwable;
}
};
This would allow for both return types on the same object:
const mockPromise = new Promise((resolve) => {
setTimeout(() => {
resolve({ hey: "Bedesqui" });
}, 1000);
})
async function main() {
const [data, error] = await trytm(mockPromise);
const result = await trytm(mockPromise);
}
The only bad part about this is that when autocompleting the properties the return object has, it will show:
-
data -
error -
Symbol
Which is not pretty... wdyt?
Okay, got a solid implementation of this working
class TryResult<T extends any | null> {
private [Symbol.iterator] = function* (): IterableIterator<[T | null, Error | null]> {
yield [this.data, this.error];
}
public data: T | null;
public error: Error | null;
constructor(data: T, error: Error | null) {
this.data = data;
this.error = error;
}
}
export const trytm = async <T>(
promise: Promise<T>,
): Promise<TryResult<T | null>> => {
try {
const data = await promise;
return new TryResult(data, null);
} catch (throwable) {
if (throwable instanceof Error) {
return new TryResult(null, throwable);
}
throw throwable;
}
};
const mockPromise = new Promise<string>((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
})
async function main() {
const [[data, error]] = await trytm(mockPromise);
const result = await trytm(mockPromise);
}
This solves Symbol appearing, but introduces you having to destructure like [[data, error]] to get the correct types :[
I'm loving the direction this is taking, I'm sold on having both possibilities returned by the same function.
I'll try to find a way to not have the Symbol in autocomplete. Also thank you so much for exploring these ideas! IMO, between it and the [[ ]] I'd take the Symbol.
Ha, what a puzzle! The only way I could get it working with both returns with correct return types is with this implementation
class TryResult<T extends any | null> {
[Symbol.iterator] = function* () {
yield this.data
yield this.error
return this.error
}
public data: T | null;
public error: Error | null;
constructor(data: T, error: Error | null) {
this.data = data;
this.error = error;
}
}
type TryMixedResult<T> = [T | null, Error | null] & TryResult<T>;
export const trytm = async <T>(
promise: Promise<T>,
): Promise<TryMixedResult<T>> => {
try {
const data = await promise;
return new TryResult(data, null) as any;
} catch (throwable) {
if (throwable instanceof Error) {
return new TryResult(null, throwable) as any;
}
throw throwable;
}
};
const mockPromise = new Promise<string>((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
})
async function main() {
const [data, error] = await trytm(mockPromise);
const result = await trytm(mockPromise);
}
But the autocomplete of result is now complete junk, and trying to Pick<T,U> or Omit<T,U> makes the return of the tuple not work, starting to think it isn't possible...


Here's an alternative approach:
type SuccessResult<T, _E> = [T, null] & {
data: T;
error: null
}
type ErrorResult<_T, E> = [null, E] & {
data: null;
error: E
}
type TryResult<T, E> = SuccessResult<T, E> | ErrorResult<T, E>
async function tryTm<T, E = Error>(promise: Promise<T>): Promise<TryResult<T, E>> {
try {
const data = await promise;
const returnable = { data, error: null } as SuccessResult<T, E>;
returnable[0] = data;
returnable[1] = null;
return returnable
} catch (throwable) {
const error = throwable as E;
const returnable = { data: null, error } as ErrorResult<T, E>;
returnable[0] = null;
returnable[1] = error;
return returnable
}
}
const mockPromise = (async () => {
return { greeting: 'Hello' }
})()
async function test() {
const result = await tryTm(mockPromise)
const { data, error } = result;
const [data2, error2] = result;
}
https://www.typescriptlang.org/play?#code/C4TwDgpgBAygrgYwRAzigSquAbYAeAFQBooB9AUQD4oBeKAbWKgDsdsBdKAMigG8AoKEKgATAIbAxALigEA3IOEQATsoD2yma2zZ+AX36hIUcqo2YUOfKSZVaDbdhLlOPAcNETpLNgo8r1TRN9Q3BoAmUQCytCZ2o6eCRUDCxcWJNqAB8TM2VotNtKfn4xFBBmBCgAMzgK4ABLNWYoYEiCAFt08ntTQMoACjB1dvqUCBkABWHRiEJKAEpJ6bHCSPz8Qup3YVaQPkUPIQQmlGBPSXsxAHcxerOhtRGxv0Ojk7PlCGA4ZWYxACNsNA6LxzmISAENFo2FA9FBSrBEMg0OsupQXq9Pt9fgCgfQAAycOjiSQYw5Yn5-QEQegARiJPh0ZI8FN+UFZVKBB1hUAQEgQAAsoP1gAL1DdqfN9q83sxTlBIcp7KLxbjoAjyMzhMc5R8vpS1fZQSTvI4IbkeRrcqjClqhBy1QSGY47ez9TjqXSGYrXRy3djORBuQYDPwdfL2moEABrKaPGb2fqlcqVfpSmhbbl+0EAc0+X3qzBzMgA5AAJCA6NQl2H6eZp4rJirVWoIBpNFqoYBp6Xa95uyy4S43O4tNrtfqRmNxp4Qebc8NnY1ec2BHl0T6D4BkxcME0AJlXGn3DM3VgUeiAA
you're a BEAUTIFUL human being
oh, I just tested it; ends up with the same problem I bumped into before :[

I think at this point we just accept the Symbol in autocomplete. It's ugly but it's technically supposed to be there... Since types are supposed to say the truth we shouldn't lie to our consumer about Symbol existing or not
@henrycunh Try this
type SuccessResult<T, _E> = {
data: T;
error: null;
} & [T, null];
type ErrorResult<_T, E> = {
data: null;
error: E;
} & [null, E];
type TryResult<T, E> = SuccessResult<T, E> | ErrorResult<T, E>;
export async function tryTm<T, E = Error>(
promise: Promise<T>,
): Promise<Omit<TryResult<T, E>, keyof typeof Array.prototype>> {
try {
const data = await promise;
const returnable = [data, null] as SuccessResult<T, E>;
returnable.data = data;
returnable.error = null;
return returnable;
} catch (throwable) {
const error = throwable as E;
const returnable = [null, error] as ErrorResult<T, E>;
returnable.data = null;
returnable.error = error;
return returnable;
}
}
weird @arn4v just tested here and the array is not showing up, did you paste the right snippet of code?

Omitting keys of Array.prototype also omits Symbol.iterator so TS thinks its no longer destructureable as an Array. (I can still see access by index though result[0]/result[1])
I personally don't care about the autocomplete polution + it makes sense to let it be for technical correctness' sake.
Edit:
Found a feature request from 7 years ago that was rejected for the same reason (tuples are just arrays, so those methods are supposed to be there): https://github.com/microsoft/TypeScript/issues/6325#issuecomment-168588269.
random question as well, does it matter if you return a tuple or object? why not return an object that satisfies both and the user can just pick depending on how they use it?
may also make typing it more straightforward and keep backwards compatibility
(not too sure about the practicality of implementing that though)
I think at this point we just accept the Symbol in autocomplete. It's ugly but it's technically supposed to be there... Since types are supposed to say the truth we shouldn't lie to our consumer about Symbol existing or not
In the example I gave with this problem, the return signature of the tuples' items were T | Error | null so not perfect as well :[
So, summarizing alternatives:
- Having both items in the
[data, error]type asT | Error | null - Having polluted autocomplete
- Having to use
[[data, error]]
Those are all the choices, I think
This is not dead!
I'm focusing on another project rn but will come back to finish this soon™.
So, summarizing alternatives:
- Having both items in the
[data, error]type asT | Error | null- Having polluted autocomplete
- Having to use
[[data, error]]
TBH I don't like any of these compromises so I'm leaning toward not implementing this into the lib, and instead adding a function that returns the desired object in the "Hey, this should be copy pasted instead of imported" section of the README.
Alternatively, I'm considering dropping the "a single function" part of the lib and exposing this as another import. Maybe something like:
import { trytm } from "@bdsqqq/try" // tuple return
import { trytm } from "@bdsqqq/try/tuple" // tuple return
import { trytm } from "@bdsqqq/try/obj" // obj return
This way we don't have to do any hacky types.
Let me know what you think! and thanks all for the feedback and resources, I'm learning a lot through this PR <3.