Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Feb 26, 2023

Error handling with Async/Await in JS

Error handling with Async/Await in JS
Автор:
Ian Segers
Источник:
Просмотров:
3323

Error handling with Async/Await in JS

This will be a small article, based on some issues I picked up during code reviews and discussions with other developers. This article is rather focused on the novice JS developers.

A Simple Try Catch

Let’s start with the simple try...catch example.

function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

try {
    thisThrows();
} catch (e) {
    console.error(e);
} finally {
    console.log('We do cleanup here');
}

// Output:
// Error: Thrown from thisThrows()
//   ...stacktrace
// We do cleanup here

This works as expected, we call the function thisThrows() which throws a regular error, we catch it, log the error and optionally we run some code in the finally block. No rocket science here.

A Try Catch with a Rejecting Promise

Now we modify thisThrows() so it actually rejects with a promise instead of a regular error. For simplicity, I will make the thisThrows() function async. Remember that an async function always returns a promise:

  • When no return statement defined, or a return statement without a value, it returns a resolving promise, equivalent to return Promise.Resolve() .
  • When a return statement is defined with a value, it will return a resolving promise with the given return value, equivalent to return Promise.Resolve("My return String")
  • When an error is thrown, a rejected promised will be returned with the thrown error, equivalent to return Promise.Reject(error) .
async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

try {
    thisThrows();
} catch (e) {
    console.error(e);
} finally {
    console.log('We do cleanup here');
}

// output:
// We do cleanup here
// UnhandledPromiseRejectionWarning: Error: Thrown from thisThrows()

Now we have the classic problem, thisThrows returns a rejecting promise, so the regular try...catch is not able to catch the error. As thisThrows() is async , so when we call it, it dispatches a promise, the code does not wait, so the finally block is executed first and then the promise executes, which then rejects. So we don’t have any code that handles this rejected promise.

We can handle this in two ways:

  • We call thisThrows() in an async function and await the thisThrows() function.
  • We chain the thisThrows() function call with a .catch() call.

The first solution would look like this:

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

async function run() {
    try {
        await thisThrows();
    } catch (e) {
        console.error(e);
    } finally {
        console.log('We do cleanup here');
    }
}

run();

// Output:
// Error: Thrown from thisThrows()
//   ...stacktrace
// We do cleanup here

And the second one:

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

thisThrows()
    .catch(console.error)
    .then(() => console.log('We do cleanup here'));

// Output:
// Error: Thrown from thisThrows()
//   ...stacktrace
// We do cleanup here

Both solutions work fine, but the async/await one is easier to reason about (at least in my personal opinion).

Caveats

We had a look at simple error handling with and without errors. Let’s have a look now at some special cases.

Returning from an async function

Let’s start with a brain teaser, what will happen with the following code?

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

async function myFunctionThatCatches() {
    try {
        return thisThrows();
    } catch (e) {
        console.error(e);
    } finally {
        console.log('We do cleanup here');
    }
    return "Nothing found";
}

async function run() {
    const myValue = await myFunctionThatCatches();
    console.log(myValue);
}

run();

At first, we might expect the output to be:

We do cleanup here
Nothing Found

But instead, we got a UnhandledPromiseRejection ! But why? Let’s step through the code:

  • thisThrows() is an async method
  • We throw an error in thisThrows()
  • As thisThrows() is async the thrown error is returned as a rejected promises from the function.
  • We return that rejected promised in myFunctionThatCatches() with the return statement.
  • Our rejected promise reaches the await keyword. The await keyword discovers that the Promise is resolved with the status “rejected” and propagates the error as an unhandled promise rejection.

Based on how our code is structured, our error snuck past our try...catch block and propagated further down in the call tree. Not good!

We solve this by using the await keyword in the return statement.

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

async function myFunctionThatCatches() {
    try {
        return await thisThrows(); // <-- Notice we added here the "await" keyword.
    } catch (e) {
        console.error(e);
    } finally {
        console.log('We do cleanup here');
    }
    return "Nothing found";
}

async function run() {
    const myValue = await myFunctionThatCatches();
    console.log(myValue);
}

run();

// Outptut:
// Error: Thrown from thisThrows()
//   ...stacktrace
// We do cleanup here
// Nothing found

Now our try..catch works as expected. Now the await keyword on line 7 will first await the returned promise of thisThrows() and if that promise rejects, the error will be propagated to the catch block. Problem solved!

Resetting your stack trace

How to handle these use cases will be different on a per-case basis, make sure to be aware of this common mistake and then decide if it’s relevant for you or not.

It’s not uncommon to see code where someone catches an error and wraps it in a new error, like in the following snippet:

function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

function myFunctionThatCatches() {
    try {
        return thisThrows();
    } catch (e) {
        throw new TypeError(e.message);
    } finally {
        console.log('We do cleanup here');
    }
}

async function run() {
    try {
        await myFunctionThatCatches();
    } catch (e) {
        console.error(e);
    }
}

run();

// Outputs:
// We do cleanup here
// TypeError: Error: Thrown from thisThrows()
//    at myFunctionThatCatches (/repo/error_stacktrace_1.js:9:15) <-- Error points to our try catch block
//    at run (/repo/error_stacktrace_1.js:17:15)
//    at Object.<anonymous> (/repo/error_stacktrace_1.js:23:1)

Notice that our stack trace only starts where we caught the original exception. When we create an error and catch it, we lose the original stack trace as we now create a new error of type TypeError and only keep the original error message (sometimes we don’t even do that).

Imagine that the thisThrows() function has far more logic in it, and somewhere in that function an error is thrown, we won’t see in our logged stack trace the origin of the problem, as we created a new error which will build a brand new stack trace. If we just re-throw our original error, we won’t have that problem:

function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

function myFunctionThatCatches() {
    try {
        return thisThrows();
    } catch (e) {
        // Maybe do something else here first.
        throw e;
    } finally {
        console.log('We do cleanup here');
    }
}

async function run() {
    try {
        await myFunctionThatCatches();
    } catch (e) {
        console.error(e);
    }
}

run();

// Outputs:
// We do cleanup here
// Error: Thrown from thisThrows()
//     at thisThrows (/repo/error_stacktrace_2.js:2:11) <-- Notice we now point to the origin of the actual error
//     at myFunctionThatCatches (/repo/error_stacktrace_2.js:7:16)
//     at run (/repo/error_stacktrace_2.js:18:15)
//     at Object.<anonymous> (/repo/error_stacktrace_2.js:24:1)

Notice that the stack trace now points to the origin of the actual error, being on line 2 from our script.

It is important to be aware of this problem when handling errors. Sometimes this might be desirable, but often this obfuscates the origin of your problem, making it hard to debug the root of a problem. If you create your own custom errors for wrapping errors, make sure you keep track of the original stack trace so that debugging doesn’t turn into a nightmare.

Summary

  • We can use try...catch for synchronous code.
  • We can use try...catch (in combination with async functions) and the .catch() approaches to handle errors for asynchronous code.
  • When returning a promise within a try block, make sure to await it if you want the try...catch block to catch the error.
  • Be aware when wrapping errors and rethrowing, that you lose the stack trace with the origin of the error.
Похожее
Apr 29, 2023
Author: Joel Olawanle
JavaScript is a versatile programming language that allows developers to create dynamic and interactive web applications. One common task in web development is to refresh or reload a web page, either to update its content or to trigger certain actions....
Dec 16
Author: Onix React
JavaScript is one of the most popular programming languages in web development, known for its flexibility and wide application across browsers and servers. But with great power comes great responsibility — maintaining clean, efficient, and readable code is crucial, especially...
Aug 28
Author: Tushar Kanjariya
Step-by-step guide to creating custom context menus (right-click menu) in JavaScript. Hello Developers 👋, A context menu, also known as a right-click menu or pop-up menu, is a menu that appears when the user performs a specific action, such as...
Mar 30, 2021
Author: Nastassia Ovchinnikova
Let’s start the battle TypeScript vs JavaScript with an introduction of both technologies. JavaScript is a scripting language, that was developed by EMCA’s Technical Committee and Brendan Eich. It works perfectly in web-browsers without the help of any web-server or...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
GraphQL решает кучу проблем — рассказываем, за что мы его любим
Типичные взаимные блокировки в MS SQL и способы борьбы с ними
9 главных трендов в разработке фронтенда в 2024 году
Из интровертов в менторы: как мидлы становятся сеньорами
NULL в SQL: что это такое и почему его знание необходимо каждому разработчику
Модуль, пакет, библиотека, фреймворк: разбираемся в разнице
Универсальный ускоритель инженера: как расти быстрее с помощью проектов
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
5 приемов увеличения продуктивности разработчика
Почему вы никогда не должны соглашаться на собеседования с программированием
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile