DEV Community

Charles Loder
Charles Loder

Posted on

Handling errors: best practices?

I'll often find myself having to handle a few api calls, and I'm not really sure what the best error handling practices are.

Below is an example of a pattern I find myself writing a lot — treating throws like returns and allowing the errors to "bubble up."

Generally, it works for me, but I'm not sure if there is a better way to handle them. Or some standard practices.

Any advice or thoughts would be appreciated!

async function someApiReq(id) {
  try {
    const resp = fetch('https://foo.com/api' { body: JSON.stringify(id) });
    if(!resp.ok) throw resp;
    return await resp.json().data
  } catch(e) {
    if(e instanceof ApiError) {
      throw e.apiMessage
    }
    throw e
  }
}

async function someOtherApiReq(id) {
  try {
    const resp = fetch('https://bar.com/api' { body: JSON.stringify(id) });
    if(!resp.ok) throw resp;
    return await resp.json().data
  } catch(e) {
    throw e
  }
}

(async () => {
  try {
    const id = 1;
    const data = someApiReq(id);
    const otherData = someOtherApiReq(data.id);
    console.log(otherData);
  } catch(e) {
    console.error(e)
  }
})();
Enter fullscreen mode Exit fullscreen mode

Oldest comments (1)

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

It just depends on the use-case.

Imagine you have a couple of error pages, and you want to redirect to this error page if you face any error.

You could do something like that:

// Check if the error is aligned with an available error page
if (res.status === 404 || res.status === 400 || res.status === 500) { 
  // redirect to that page
  res.redirect(302, `/${res.status}`);
}
Enter fullscreen mode Exit fullscreen mode

You can also use the QueryString to add a parameter with a message if you feel it necessary:

// Check if the error is aligned with an available error page
if (res.status === 404 || res.status === 400 || res.status === 500) { 
  // redirect to that page
  res.redirect(302, `/${res.status}?message=${error}`);
}
Enter fullscreen mode Exit fullscreen mode

That's for error handling from the functional PoV.

On the other hand, from the code and user journey PoV, using Throw means is stops the execution plus you always will need to catch it anywhere you're using it, so you'll need to handle it multiple places with all probability.

Depending on the use-case may be better to just print an error for debugging purposes and continue the program execution, maybe adding a "retry" button in case some error occurs or retrying each few seconds programmatically after a failure.

In case you do retry the request programmatically, you need to manage the API response properly. E.g. If you got a 400 - bad request you should not attempt the same request again See the reference.

Last but not least you can manage your software functions to always return something, thus avoiding the software to stop the execution as well and providing workarounds regarding the user journeys. See the quick example below:

/**
 * Evaluates a password recovery link
 * @param {Response} res
 * @param {string} token
 * @returns {Object}
 */
const evaluateRequest = async (res, token) => {
  let answer;
  try {
    const decoded = jwt.verify(token, serverRuntimeConfig.secret);
    let parts = decoded?.sub?.split('#').filter((p) => p !== '');
    const validated = validateParts(parts);

    if (validated) answer = { code: 302, route: 'forgot-password', message: token };
    else answer = { code: 302, route: 'forgot-password', message: 'Wrong token' };

  } catch (error) {
    answer = { code: 302, route: 'forgot-password', message: 'Token expired' };
  } finally {
    return answer;
  }
};

// Endpoint input/output handler:
export default function myApiHandler(req, res) {
  switch (req.method) {
    case 'GET':
      const result = await evaluateRequest(res, req.query.token);
      return (result.code !== 302 && res.status(result.code).end(result.message)) || res.redirect(302, `${result.route}?error=${result.message}`);
    case 'POST':
       // do whatever
    default:
      return res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}
Enter fullscreen mode Exit fullscreen mode

I didn't check if the example makes sense 100% but I think it catches the idea 🤣 neither I covered all the possibilities on the comment, I always prefer the program to continue while handling the use cases, it's better for the user experience and overall satisfaction.

Hope it helps somehow, best regards 😄