DEV Community

Cover image for Say goodbye Trycatch Hell

Say goodbye Trycatch Hell

Ivan Zaldivar on September 18, 2021

Hi everyone! It is possible that it is here for the post title, and I daresay you have had problems handling errors with async/await. In this post ...
Collapse
 
oysterd3 profile image
Oyster Lee

This is over-engineered to me

Collapse
 
pedro profile image
Pedro M. M. • Edited

Actually, this error tuple-pattern is very common in Go.

if x, err := doSomething(); err != nil {
   // failed!
}
// worked! Safe to use x.
Enter fullscreen mode Exit fullscreen mode
x, err := doSomething()
if err != nil {
  // failed!
}
// worked! Safe to use x
Enter fullscreen mode Exit fullscreen mode
x, err := doSomething(); err == nil {
  // Do something with x only if doSomething() returns no errors
}
Enter fullscreen mode Exit fullscreen mode
if x, ok := booleanOp(); ok {
    // Do something with x only if ok = true
} 
Enter fullscreen mode Exit fullscreen mode

In terms of error handling, my favorite is rust because it has less overhead. But it's uglier and a little bit more complicated.

Collapse
 
oysterd3 profile image
Oyster Lee

Yes, I am a gopher, but do you think over 50% JavaScript developer using Go?

Thread Thread
 
pedro profile image
Pedro M. M. • Edited

No! Of course not lol.

My point was — If a whole modern language, which is known as an easy-to-read language, uses a pattern like that as standard good practice maybe the pattern is not that over-engineered

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

It seems like this at first glance, but it will be useful when the use of trycatch becomes repetitive, and these functions allow you to always reuse them.

Collapse
 
sannajammeh profile image
Sanna Jammeh

It's a great solution, but it can be made even simpler. You will need another if statement to handle errors in your example. Why not just check if the promise is an error instance.

const res = await (promise).catch((e) => e).

If(res instanceof Error) {
// Handle err
}

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

Mm... I've never thought about it, I like your solution. Thanks!

Collapse
 
szymondziewonski profile image
Szymon Dziewoński

this exactly how I catch errors, and I was wondering whole time reading article "why is there a problem?" However I like wrapper util :)

Collapse
 
stradivario profile image
Kristiqn Tachev • Edited

You need to catch exceptions on the top level of the function and try not to use the try catch scope until you need to re throw something specific.

With the following approach we are hiding unhandled rejections by providing status code 500 (Database errors for example) and only the correct thrown exceptions will be shown as a result of the requester.

import { randomBytes } from "crypto";

const generateUID = () => randomBytes(8).toString("hex");

/* @HttpCodes
 * Partial list of http codes
 */
export enum HttpCodes {
  Ok = 200,
  Redirect = 302,
  BadRequest = 400,
  Unauthorized = 401,
  PaymentRequired = 402,
  Forbidden = 403,
  NotFound = 404,
  MethodNotAllowed = 405,
  NotAcceptable = 406,
  ProxyAuthenticationRequired = 407,
  RequestTimeout = 408,
  Conflict = 409,
  Gone = 410,
  LengthRequired = 411,
  PreconditionFailed = 412,
  PayloadTooLarge = 413,
  URITooLong = 414,
  UnsupportedMediaType = 415,
  RangeNotSatisfiable = 416,
  ExpectationFailed = 417,
  ImATeapot = 418,
  MisdirectedRequest = 421,
  UnprocessableEntity = 422,
  Locked = 423,
  FailedDependency = 424,
  UnorderedCollection = 425,
  UpgradeRequired = 426,
  PreconditionRequired = 428,
  TooManyRequests = 429,
  RequestHeaderFieldsTooLarge = 431,
  UnavailableForLegalReasons = 451,
  InternalServerError = 500,
  NotImplemented = 501,
  BadGateway = 502,
  ServiceUnavailable = 503,
  GatewayTimeout = 504,
  HTTPVersionNotSupported = 505,
  VariantAlsoNegotiates = 506,
  InsufficientStorage = 507,
  LoopDetected = 508,
  BandwidthLimitExceeded = 509,
  NotExtended = 510,
  NetworkAuthenticationRequired = 511,
}

/* @HttpError
 * Main generic http status error handler
 */
export class HttpError extends Error {
  public reason?: string;
  public isCustomError? = true;
  constructor(
    public readonly message: string | never,
    public readonly statusCode: HttpCodes,
    public readonly details?: object,
  ) {
    super(message);
    this.name = HttpCodes[statusCode];
    this.message = message;

    /* When message is different than status text we have a custom message reason */
    if (HttpCodes[statusCode] !== message) {
      this.reason = message;
    }
  }
}

/* @HttpCodeEnumType
 * Generic enumerable for strict checking of all http codes
 * example: throw HttpErrors.Unauthorized()
 */
export type HttpCodeEnumType<T, K> = {
  [key in keyof typeof HttpCodes]: (
    message?: T,
    details?: object | undefined,
  ) => K;
};

/* @HttpErrors
 * Record of httpErrors
 * example: throw HttpErrors.Unauthorized()
 */
export const HttpErrors: HttpCodeEnumType<string, HttpError> = Object.values(
  HttpCodes,
)
  .filter(k => typeof HttpCodes[k as never] === 'number')
  .reduce((codes, code) => ({
      ...codes,
      [code]: (message = code as never, details: never) =>
        new HttpError(message, HttpCodes[code as never] as never, details),
    }), {} as HttpCodeEnumType<string, HttpError>);


async function myAsyncFunctionNext() {
  return 1;
}
async function myAsyncFunctionNext2() {
  return 2;
}
async function myAsyncFunctionNext3() {
  throw HttpErrors.BadGateway()
  return 3;
}

async function myAsyncFunctionNext4() {
  throw new Error('unauthorized')
}


async function myAsyncFunction() {
  await myAsyncFunctionNext();
  await myAsyncFunctionNext2();
  await myAsyncFunctionNext3();
  // Only try catch if you really want to change the error from specific message thrown
  try {
    await myAsyncFunctionNext4()
  } catch (e) {
    if (e.message === 'unauthorized') {
      throw HttpErrors.Unauthorized()
    }
    throw e;
  }
  return 'successs';
}

export async function handler() {
  try {
    return {
      statusCode: 200,
      body: await myAsyncFunction()
    }
  } catch (e) {
    if (e.isCustomError) {
      return {
        statusCode: e.statusCode,
        body: JSON.stringify(e),
      };
    }

    const uuid = [generateUID(), "-", generateUID(), "-", generateUID()].join(
      ""
    );
    console.error(`[${uuid}]:`, e);
    return {
      statusCode: 500,
      body: JSON.stringify({
        name: "InternalServerError",
        details: { uuid },
      }),
    };
  }
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
lexlohr profile image
Alex Lohr

There's a simple solution. Whatever you await should usually return a promise and thus can be directly caught:

// Don't
try {
  await myAsyncFn();
} catch(e) {
  console.error(e);
}

// Do
await myAsyncFn().catch(console.error);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
r37r0m0d3l_77 profile image
Anton Trofimenko • Edited

Done years ago of.js.org

Collapse
 
laurentperroteau profile image
Laurent Perroteau

Another exemple : await-to-js that returns the error first to enforce it to be handled :

const [error, result] = await to(getList());

if (error) ...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hilleer profile image
Daniel Hillmann • Edited

Was about to point out it all seemed a bit like re-inventing the wheel, considering modules like this already exist.

I personally use and like await-to-js for many use cases.

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

I like, but is not necessary why is Typescript being used (as long as the developer uses static typing)

Collapse
 
ivanhoe011 profile image
Ivan Dilber

Completely pointless refactor exercise IMHO, as now instead of try/catch-ing you need to check each time if a function returned an error or not.

When an exception is thrown it will jump out of all blocks, so there's no need to catch it at the same level. You can set just a single try/catch block on the parent (or even higher) scope to catch all of them.

Collapse
 
gers2017 profile image
Gers2017 • Edited

I usually do

async function handleRequest(req: Response, res: Request){
   const {user, error} = await getUserData(1);
   if(error){
       return res.status(400).json({error})
   }
   res.status(200).json({user})
}

interface UserData{
    name:string
    age: number
}

type UserResponse = { user: UserData | null, error: string | null }
async function getUserData(id: number): Promise<UserResponse>{
    if(id <= 0){
        return {
            user: null,
            error: "Wrong, id can't be less or equal to zero!!!" // Why this failed
        }
    }
    return {
        user: {name: "Jimmy", age: 22},
        error: null
    }
}
Enter fullscreen mode Exit fullscreen mode

Or I'd just throw an error inside the getUserData function

async function handleRequest(){
    try{
        const { user } = await getUserData(1);
        const { user: user2 } = await getUserData(1);
        console.log(user, user2);
    }
    catch(e){
        console.error(e);
    }
}

interface UserData{
    name:string
    age: number
}

type UserResponse = { user: UserData | null }
async function getUserData(id: number): Promise<UserResponse>{
    if(id <= 0){
        throw Error("Wrong, id can't be less or equal to zero!!!")
    } else {
        return { user: {name: "Jimmy", age: 22} }
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ivanzm123 profile image
Ivan Zaldivar

Thank you for your contribution, my friend.

Personally, I do not like to handle errors that way, I consider them incorrect, I leave you the link so you can take a look about making these exceptions: developer.mozilla.org/docs/Web/Jav...

async function getUser(id: number): Promise<User> {
  // We throw an exception.
  if (id <= 0) throw new Error("Wrong, id can't be less or equal to zero!!!");
  // Result.
  return { id, email: "abc@domain.com" };
}

Enter fullscreen mode Exit fullscreen mode

Now we handle it.

async function handleRequest(req: Request, res: Response) {
  const [user, err] = await promHandler(getUser(Number(req.params.id)));
  if (!user || err) return res.status(400).json({
    name: "BadRequest",
    message: err.message
  });
  res.status(200).json(user);
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
gers2017 profile image
Gers2017

Sure, I actually like the golang pattern because is easy to read.
Also I got a different approach by asking about this.

type Result<T> = 
{ result: T, error?: undefined } ||  
{ result?: undefined, error: Error }
Enter fullscreen mode Exit fullscreen mode
async function getUserData(id: number): Promise<Result<UserData>>{
    if(id <= 0){
        return {
            error: new Error("Wrong, id can't be less or equal to zero!!!");
        }
    }
    return {
        result: {name: "Jimmy", age: 22},
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vnues profile image
vnues • Edited

This is too complicated, I usually do this

const to = (promise: any) => {
  if (!promise) {
    return new Promise((resolve, reject) => {
      reject(new Error('requires promises as the param'))
    }).catch((err: Error) => {
      return [err, null]
    })
  }
  return promise
    .then(function() {
      const arr = Array.from(arguments)
      return [null, ...arr]
    })
    .catch((err: Error) => {
      return [err, null]
    })
}

export const exec = async (sql: string) => {
  const [err, res] = await to(db.exec(sql))
  if (err) {
    throw new Error(err)
  }
  return res
}

// ...async function 
const [data,err] = await to(getUsers())
Enter fullscreen mode Exit fullscreen mode

Because you have to know, promsie comes with try/catch

Collapse
 
valery profile image
Валерий Кулаков
  • First, you have not avoided the problem, because now You have to write some if (error) in the code, and it will be even worse.
  • Moreover, in fact, we most likely need a global error handler.
Collapse
 
lil5 profile image
Lucian I. Last

Starts to look a lot like GoLang, nice

Although I find your handler example to be a bit over complicated.

Collapse
 
adi518 profile image
Adi Sahar

This matter has been discussed many times. It's better to return the error as the first element to coerce consumers into always handling errors, otherwise it's easy to forget.

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

In this case, it will be hard to forget, since Typescript is used, so you will know that it is the answer or a null value. Thank you for your comments.

Collapse
 
chuckytuh profile image
João Gonçalves

github.com/supermacro/neverthrow is something to look at, not only avoids the trycatch hell but it also provides a way to easily compose computations!

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

I've been going through the package and it looks interesting. Thanks for your contribution.

Collapse
 
jaecktec profile image
Constantin

Love it, in golang people discuss if they want exceptions or not, in Javascript you have them and can decide to just not to.

Collapse
 
fkereki profile image
Federico Kereki

This is going in the direction of Maybe or Either monads.

Collapse
 
ricky11 profile image
Rishi U

you lost me at interfaces, not that i didn't understand, but just lost me.

Collapse
 
defite profile image
Nikita Makhov • Edited

Goodbye trycatch hell, hello Promise-async-await hell...

Collapse
 
cyrstron profile image
cyrstron

Not sure about golang, but you don't need this many try/catch in JS. Just handle an error only where you need to handle it. Otherwise let it go to a concerned parent.

Collapse
 
abraaoalves profile image
AbraaoAlves

This is very similar a post on f# blog some years ago ... fsharpforfunandprofit.com/rop/