When you are coming from languages like Java, C++, or C#, you are used to doing your error handling by throwing exceptions. And subsequently, catching them in a cascade of catch
clauses. There are arguably better ways to do error handling, but this one has been around for ages and given history and influences, has also found its way into JavaScript.
So, this is a valid way of doing error handling in JavaScript and TypeScript. But try to follow the same flow as with other programming languages, and annotate the error in your catch
clause.
try {
// something with Axios, for example
} catch(e: AxiosError) {
// ^^^^^^^^^^ Error 1196 💥
}
TypeScript will error with TS1196: Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.
There are a couple of reasons for this:
1. Any type can be thrown #
In JavaScript, you are allowed to throw every expression. Of course, you can throw “exceptions” (or errors, as we call them in JavaScript), but it’s also possible to throw any other value:
throw "What a weird error"; // 👍
throw 404; // 👍
throw new Error("What a weird error"); // 👍
Since any valid value can be thrown, the possible values to catch are already broader than your usual sub-type of Error
.
2. There is only one catch clause in JavaScript #
JavaScript only has one catch
clause per try
statement. There have been proposals for multiple catch clauses and even conditional expressions in the distant past, but they never manifested. See JavaScript — the definitive guide for – hold it! – JavaScript 1.5 – what?!?
Instead, you should use this one catch
clause and do instanceof
and typeof
checks (Source):
try {
myroutine(); // There's a couple of errors thrown here
} catch (e) {
if (e instanceof TypeError) {
// A TypeError
} else if (e instanceof RangeError) {
// Handle the RangeError
} else if (e instanceof EvalError) {
// you guessed it: EvalError
} else if (typeof e === "string") {
// The error is a string
} else if (axios.isAxiosError(e)) {
// axios does an error check for us!
} else {
// everything else
logMyErrors(e);
}
}
Note: The example above is also the only correct way to narrow down types for catch
clauses in TypeScript.
And since all possible values can be thrown, and we only have one catch
clause per try
statement to handle them, the type range of e
is exceptionally broad.
3. Any exception can happen #
But hey, since you know about every error that can happen, wouldn’t be a proper union type with all possible “throwables” work just as well? In theory, yes. In practice, there is no way to tell which types the exception will have.
Next to all your user-defined exceptions and errors, the system might throw errors when something is wrong with the memory when it encountered a type mismatch or one of your functions has been undefined. A simple function call could exceed your call stack and cause the infamous stack overflow.
The broad set of possible values, the single catch
clause, and the uncertainty of errors that happen only allow two possible types for e
: any
and unknown
.
What about Promise rejections? #
The same is true if you reject a Promise. The only thing TypeScript allows you to specify is the type of a fulfilled Promise. A rejection can happen on your behalf, or through a system error:
const somePromise = () => new Promise((fulfil, reject) => {
if (someConditionIsValid()) {
fulfil(42);
} else {
reject("Oh no!");
}
});somePromise()
.then(val => console.log(val)) // val is number
.catch(e => {
console.log(e) // e can be anything, really.
})
It becomes clearer if you call the same promise in an asnyc
/await
flow:
try {
const z = await somePromise(); // z is number
} catch(e) {
// same thing, e can be anything!
}
Bottom line #
Error handling in JavaScript and TypeScript can be a “false friend” if you come from other programming languages with similar features. Be aware of the differences, and trust the TypeScript team and type checker to give you the correct control flow to make sure your errors are handled well enough.
In TypeScript, just like in JavaScript, the try catch statement is used to handle errors in your code. It consists of three statements:
- The try statement that defines the code to try (or run).
- The catch statement that defines the code that handles the errors.
- The optional finally statement that defines the code that runs at the end either way.
Here is an example of the try catch finally statements in action.
typescripttry {
functionThatThrowsError();
} catch (e: any){
console.log(e);
} finally {
console.log('finally');
}
In this article, I will go over, in detail, about how does the try catch statement work, answer common questions, as well provide TypeScript specific examples.
Let’s get to it 😎.
Page content
- The definition
- How to catch a specific error?
- How to use try catch with async/await?
- What are some best practices?
- Final thoughts
The definition
A try catch statement handles errors in a specific code block.
It consists of two mandatory statements, try and catch, and it follows this flow.
- The code inside the try statement runs.
- If the try statement throws a runtime error, the code block inside the catch statement executes.
Here is the basic syntax for a try catch statement.
typescripttry {
// code to try
} catch {
// code that handles the error
}
As you can see, the catch parameter is optional.
This syntax, called optional catch binding, may require a polyfill for older browsers.
Alternatively, you can set up a catch parameter, if you need one.
typescripttry {
// code to try
} catch (e: any) {
// code that handles the error
console.log(e.name);
console.log(e.message);
}
In TypeScript, a catch parameter can only be of type any or unknown.
An Error object has two properties that you can use (message and name).
There are two important things to know about the try catch statement:
- It only works for runtime errors, this means that it will not work for syntax errors.
- It only works for synchronous code.
You can put almost any code statements that you want inside the try block… For loop, switch statement, while, etc…
The finally statement
The finally statement ALWAYS runs regardless after a try catch statement.
The code inside the finally statement will even run if the catch statement throws an error or returns.
typescripttry {
// code to try
} catch (e: any) {
// code that handles the error
} finally {
// code that will run regardless
}
The throw statement
The throw statement allows the developer to create custom errors that you can throw in your code.
Here is an example of this.
typescripttry {
throw Error('This is an error');
} catch (e: any) {
console.log('error');
console.log(e.message);
} finally {
console.log('finally');
}
// outputs: 'error'
// outputs: 'This is an error'
// outputs: 'finally'
How to catch a specific error?
You can catch a specific error inside a catch statement by using the instanceof operator.
typescripttry {
// Code
} catch (e: any) {
if (e instanceof YourException){
// code that runs for this exception.
} else if (e instanceof YourException2){
// code that runs for this exception.
}
}
As you can see, we are executing different parts of the code based on the error that we get.
How to use try catch with async/await?
The try catch will catch all errors inside an async function. We just need to be sure to await the function.
Here is an example of an async function inside a try catch statement.
typescripttry {
await createUser({
name: 'Tim Mousk'
});
} catch (e: any) {
// handles the error
console.log(e);
}
Alternatively, you can use the promise catch built-in method.
typescriptawait createUser({
name: 'Tim Mousk'
}).catch((e: any) => {
// handles the error
console.log(e);
});
What are some best practices?
- Throw your own custom errors, this will help you with development and debugging.
- For large applications, you need to implement remote exception logging with window.onerror. This will help you debug your application.
- Don’t use browser non-standard methods.
Final thoughts
As you can see, understanding the try catch statement is no rocket science.
It is actually quite easy to master.
You have three statements, try, catch, and finally, that do exactly the action in their name. The finally statement is optional. Also, you can throw an Error, with the throw statement.
That’s it… That’s the big picture.
When it comes to, the try catch statement, the only difference between JavaScript and TypeScript is that with TypeScript you can specify the type of the catch clause parameter.
I hope you enjoyed reading through this article, please share it with your fellow developers.
Don’t hesitate to ask questions, I always respond.
written by:
Hello! I am Tim Mouskhelichvili, a Freelance Developer & Consultant from Montreal, Canada.
I specialize in React, Node.js & TypeScript application development.
If you need help on a project, please reach out, and let’s work together.
Содержание
- TypeScript: Narrow types in catch clauses
- 1. Any type can be thrown #
- 2. There is only one catch clause in JavaScript #
- 3. Any exception can happen #
- What about Promise rejections? #
- Bottom line #
- How does try catch work in TypeScript?
- The definition
- The finally statement
- The throw statement
- How to catch a specific error?
- How to use try catch with async/await?
- What are some best practices?
- Final thoughts
- Handle Exceptions Using try..catch..finally in TypeScript
- Handle Exceptions in TypeScript
- unknown Type of catch Clause Variable
- Чистый код для TypeScript — Часть 3
- Тестирование
- Три закона TDD
- Правила F.I.R.S.T.
- Один кейс на тест
- Асинхронность
- Используйте promises а не callbacks
- Обработка ошибок
- Всегда используйте ошибки для отклонений(reject)
- Не игнорируйте отловленные ошибки
- Не игнорируйте ошибки, возникшие в промисах
- Форматирование
- Используйте один вариант именования
- Организация импортов
TypeScript: Narrow types in catch clauses
Published on March 2, 2021
Reading time: 5 minutes
When you are coming from languages like Java, C++, or C#, you are used to doing your error handling by throwing exceptions. And subsequently, catching them in a cascade of catch clauses. There are arguably better ways to do error handling, but this one has been around for ages and given history and influences, has also found its way into JavaScript.
So, this is a valid way of doing error handling in JavaScript and TypeScript. But try to follow the same flow as with other programming languages, and annotate the error in your catch clause.
TypeScript will error with TS1196: Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.
There are a couple of reasons for this:
1. Any type can be thrown #
In JavaScript, you are allowed to throw every expression. Of course, you can throw “exceptions” (or errors, as we call them in JavaScript), but it’s also possible to throw any other value:
Since any valid value can be thrown, the possible values to catch are already broader than your usual sub-type of Error .
2. There is only one catch clause in JavaScript #
JavaScript only has one catch clause per try statement. There have been proposals for multiple catch clauses and even conditional expressions in the distant past, but they never manifested. See JavaScript — the definitive guide for – hold it! – JavaScript 1.5 – what.
Instead, you should use this one catch clause and do instanceof and typeof checks (Source):
Note: The example above is also the only correct way to narrow down types for catch clauses in TypeScript.
And since all possible values can be thrown, and we only have one catch clause per try statement to handle them, the type range of e is exceptionally broad.
3. Any exception can happen #
But hey, since you know about every error that can happen, wouldn’t be a proper union type with all possible “throwables” work just as well? In theory, yes. In practice, there is no way to tell which types the exception will have.
Next to all your user-defined exceptions and errors, the system might throw errors when something is wrong with the memory when it encountered a type mismatch or one of your functions has been undefined. A simple function call could exceed your call stack and cause the infamous stack overflow.
The broad set of possible values, the single catch clause, and the uncertainty of errors that happen only allow two possible types for e : any and unknown .
What about Promise rejections? #
The same is true if you reject a Promise. The only thing TypeScript allows you to specify is the type of a fulfilled Promise. A rejection can happen on your behalf, or through a system error:
It becomes clearer if you call the same promise in an asnyc / await flow:
Bottom line #
Error handling in JavaScript and TypeScript can be a “false friend” if you come from other programming languages with similar features. Be aware of the differences, and trust the TypeScript team and type checker to give you the correct control flow to make sure your errors are handled well enough.
I’ve written a book on TypeScript! Check out TypeScript in 50 Lessons, published by Smashing Magazine
Источник
How does try catch work in TypeScript?
March 29, 2022 • 3 minutes to read
In TypeScript, just like in JavaScript, the try catch statement is used to handle errors in your code. It consists of three statements:
- The try statement that defines the code to try (or run).
- The catch statement that defines the code that handles the errors.
- The optional finally statement that defines the code that runs at the end either way.
Here is an example of the try catch finally statements in action.
In this article, I will go over, in detail, about how does the try catch statement work, answer common questions, as well provide TypeScript specific examples.
The definition
A try catch statement handles errors in a specific code block.
It consists of two mandatory statements, try and catch, and it follows this flow.
- The code inside the try statement runs.
- If the try statement throws a runtime error, the code block inside the catch statement executes.
Here is the basic syntax for a try catch statement.
As you can see, the catch parameter is optional.
, may require a polyfill for older browsers.
Alternatively, you can set up a catch parameter, if you need one.
In TypeScript, a catch parameter can only be of type any or unknown .
An Error object has two properties that you can use (message and name).
There are two important things to know about the try catch statement:
- It only works for runtime errors, this means that it will not work for syntax errors.
- It only works for synchronous code.
You can put almost any code statements that you want inside the try block. For loop, switch statement, while, etc.
The finally statement
The finally statement ALWAYS runs regardless after a try catch statement.
The code inside the finally statement will even run if the catch statement throws an error or returns.
The throw statement
The throw statement allows the developer to create custom errors that you can throw in your code.
Here is an example of this.
How to catch a specific error?
You can catch a specific error inside a catch statement by using the instanceof operator.
As you can see, we are executing different parts of the code based on the error that we get.
How to use try catch with async/await?
The try catch will catch all errors inside an async function. We just need to be sure to await the function.
Here is an example of an async function inside a try catch statement.
Alternatively, you can use the promise catch built-in method.
What are some best practices?
- Throw your own custom errors, this will help you with development and debugging.
- For large applications, you need to implement remote exception logging with window.onerror . This will help you debug your application.
- Don’t use browser non-standard methods.
Final thoughts
As you can see, understanding the try catch statement is no rocket science.
It is actually quite easy to master.
You have three statements, try, catch, and finally, that do exactly the action in their name. The finally statement is optional. Also, you can throw an Error, with the throw statement.
That’s it. That’s the big picture.
When it comes to, the try catch statement, the only difference between JavaScript and TypeScript is that with TypeScript you can specify the type of the catch clause parameter.
I hope you enjoyed reading through this article, please share it with your fellow developers.
Don’t hesitate to ask questions, I always respond.
Источник
Handle Exceptions Using try..catch..finally in TypeScript
This tutorial will discuss handling exceptions in TypeScript using the try..catch..finally statement.
Handle Exceptions in TypeScript
In TypeScript, the try..catch..finally block handles exceptions that arise in the program at run time. It lets the program run correctly and does not end it arbitrarily.
The main code where an exception could arise is placed inside the try block. If an exception occurs, it goes to the catch block where it is handled; however, the catch block is skipped if no error is encountered.
The finally block will always execute in any case, whether an error arises or not in the program.
Below are some code examples of how we can use try..catch..finally for exception handling in TypeScript.
The function inside the try block is called thrice, where it passes an argument. If the generated number is greater than 0.5, it will execute the if block and return true .
Otherwise, it will throw an error, which is handled at the catch block. It tells which call it is thrown after the execution of try and catch .
Consider one more example below.
unknown Type of catch Clause Variable
Before version 4.0 of TypeScript, the catch clause variables have a type of any . Since the variables have the type any assigned to them, they lack the type-safety, leading to invalid operations and errors.
Now, version 4.0 of TypeScript allows us to specify the type unknown of the catch clause variable, which is much safer than the type any .
It reminds us that we need to perform a type-check of some sort before operating on values.
Ibrahim is a Full Stack developer working as a Software Engineer in a reputable international organization. He has work experience in technologies stack like MERN and Spring Boot. He is an enthusiastic JavaScript lover who loves to provide and share research-based solutions to problems. He loves problem-solving and loves to write solutions of those problems with implemented solutions.
Источник
Чистый код для TypeScript — Часть 3
Заключительная часть статей, посвященных тому, как можно использовать принципы чистого кода в TypeScript(ps. Все эти принципы относятся не только к языку TypeScript).
Тестирование
Тестирование важнее деплоя. Если у вас нет тестов или их мало, то каждый раз при выкладке кода на боевые сервера у вас не будет уверенности, что ничего не сломается. Решение о достаточном количестве тестов остается на совести вашей команды, но 100% покрытие тестами всех выражений и ветвлений обеспечивает высокое доверие к вашему коду и спокойствие всех разработчиков. Из этого следует, что в дополнение к отличному фреймворку для тестирования, необходимо также использовать хороший инструмент покрытия.
Нет никакого оправдания, чтобы не писать тесты. Есть много хороших фреймворков для тестирования на JS с поддержкой типов для TypeScript, так что вы найдите тот который понравится вашей команде. Когда вы найдете тот, который работает для вашей команды, тогда стремитесь всегда писать тесты для каждой новой фичи/модуля, которую вы пишете. Если вы предпочитаете метод тест-ориентированной разработки (TDD), это замечательно, но главное — просто убедиться, что вы достигли своих целей покрытия, прежде чем запускать какую-либо функцию или реорганизовать существующую.
Три закона TDD
Правила F.I.R.S.T.
Чистые тесты должны следовать правилам:
- Быстрота(Fast) Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.
- Независимость(Independent) Тесты не должны зависеть друг от друга. Они должны обеспечивать одинаковые выходные данные независимо от того, выполняются ли они независимо или все вместе в любом порядке.
- Повторяемость(Repeatable) Тесты должны выполняться в любой среде, и не должно быть никаких оправданий тому, почему они провалились.
- Очевидность(Self-Validating) Тест должен отвечать либо Passed, либо Failed. Вам не нужно сравнивать файлы логов, для чтобы ответить, что тест пройден.
- Своевременность(Timely) Юнит тесты должны быть написаны перед производственным кодом. Если вы пишете тесты после производственного кода, то вам может показаться, что писать тесты слишком сложно.
Один кейс на тест
Тесты также должны соответствовать Принципу единой ответственности(SPP). Делайте только одно утверждение за единицу теста.(ps. не пренебрегайте этим правилом)
Хорошо:
Асинхронность
Используйте promises а не callbacks
Callback-функции ухудшают читаемость и приводят к чрезмерному количеству вложенности (ад обратных вызовов(callback hell)). Существуют утилиты, которые преобразуют существующие функции, используя стиль callback-ов, в версию, которая возвращает промисы (для Node.js смотрите util.promisify , для общего назначения смотрите pify, es6-promisify)
Плохо:
Хорошо:
Промисы поддерживают несколько вспомогательных методов, которые помогают сделать код более понятным:
Методы | Описание |
---|---|
Promise.resolve(value) | Преобразуйте значение в решенный промис. |
Promise.reject(error) | Преобразуйте ошибку в отклоненный промис. |
Promise.all(promises) | Возвращает новый промис, который выполняется с массивом значений выполнения для переданных промисов или отклоняется по причине первого промиса, который выполняется с ошибкой. |
Promise.race(promises) | Возвращает новый промис, который выполнен/отклонен с результатом/ошибкой первого выполненного промиса из массива переданных промисов. |
Promise.all особенно полезен, когда есть необходимость запускать задачи параллельно. Promise.race облегчает реализацию таких вещей, как тайм-ауты для промисов.
Обработка ошибок
Бросать ошибки — хорошее решение! Это означает, что во время выполнения вы будете знать, если что-то пошло не так, вы сможете остановить выполнение вашего приложения убив процесс (в Node) в нужный момент и увидеть место ошибки с помощью стек трейса в консоли.
Всегда используйте ошибки для отклонений(reject)
JavaScript и TypeScript позволяют вам делать throw любым объектом. Промис также может быть отклонен с любым объектом причины. Рекомендуется использовать синтаксис throw с типом Error . Это потому что ваша ошибка может быть поймана в более высоком уровне кода с синтаксисом catch . Было бы очень странно поймать там строковое сообщение и сделать отладку более болезненной. По той же причине вы должны отклонять промисы с типами Error .
Плохо:
Хорошо:
Преимущество использования типов Error заключается в том, что они поддерживается синтаксисом try/catch/finally и неявно всеми ошибками и имеют свойство stack , которое является очень мощным для отладки. Есть и другие альтернативы: не использовать синтаксис throw и вместо этого всегда возвращать пользовательские объекты ошибок. TypeScript делает это еще проще.
Рассмотрим следующий пример:
Для подробного объяснения этой идеи обратитесь к оригинальному посту.
Не игнорируйте отловленные ошибки
Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль ( console.log ) не намного лучше, так как зачастую оно может потеряться в море консольных записей. Оборачивание куска кода в try/catch означает, что вы предполагаете возможность появления ошибки и имеете на этот случай четкий план.
Плохо:
Хорошо:
Не игнорируйте ошибки, возникшие в промисах
Вы не должны игнорировать ошибки в промисах по той же причине, что и в try/catch .
Плохо:
Хорошо:
Форматирование
Форматирование носит субъективный характер. Как и во многом собранном здесь, в вопросе форматирования нет жестких правил, которым вы обязаны следовать. Главное — НЕ СПОРИТЬ по поводу форматирования. Есть множество инструментов для автоматизации этого. Используйте один! Это трата времени и денег когда инженеры спорят о форматировании. Общее правило, которому стоит следовать соблюдайте правила форматирования принятые в команде
Для TypeScript есть мощный инструмент под названием TSLint. Это статический анализ инструмент, который может помочь вам значительно улучшить читаемость и поддерживаемость вашего кода. Но лучще используйте ESLint, так как TSLint больше не поддерживается.
Есть готовые к использованию конфигурации TSLint и ESLint, на которые вы можете ссылаться в своих проектах:
TSLint react — правила, связанные с React & JSX
TSLint + Prettier — правила линта для Prettier средство форматирования кода
ESLint rules for TSLint — ESLint правила для TypeScript
Immutable — правила отключения мутации в TypeScript
Обратитесь также к этому великому TypeScript StyleGuide and Coding Conventions источнику.
Используйте один вариант именования
Использование заглавных букв говорит вам о ваших переменных, функциях и др… Эти правила субъективны, поэтому ваша команда может выбирать все, что они хотят. Дело в том, что независимо от того, что вы все выберите, просто будьте последовательны.
Плохо:
Хорошо:
Предпочитайте использовать PascalCase для имен классов, интерфейсов, типов и пространств имен. Предпочитайте использовать camelCase для переменных, функций и членов класса.
Организация импортов
С помощью простых и понятных операторов импорта вы можете быстро увидеть зависимости текущего кода.
Убедитесь, что вы используете следующие хорошие практики для операторов import :
- Операторы импорта должны быть в алфавитном порядке и сгруппированы.
- Неиспользованный импорт должен быть удален.
- Именованные импорты должны быть в алфавитном порядке (т.е. import from ‘foo’; )
- Источники импорта должны быть в алфавитном порядке в группах, т.е.: import * as foo from ‘a’; import * as bar from ‘b’;
- Группы импорта обозначены пустыми строками.
- Группы должны соблюдать следующий порядок:
- Полифилы (т.е. import ‘reflect-metadata’; )
- Модули сборки Node (т.е. import fs from ‘fs’; )
- Внешние модули (т.е. import < query >from ‘itiriri’; )
- Внутренние модули (т.е. import < UserService >from ‘src/services/userService’; )
- Модули из родительского каталога (т.е. import foo from ‘../foo’; import qux from ‘../../foo/qux’; )
- Модули из того же или родственного каталога (т.е. import bar from ‘./bar’; import baz from ‘./bar/baz’; )
Источник
- Handle Exceptions in TypeScript
unknown
Type ofcatch
Clause Variable
This tutorial will discuss handling exceptions in TypeScript using the try..catch..finally
statement.
Handle Exceptions in TypeScript
In TypeScript, the try..catch..finally
block handles exceptions that arise in the program at run time. It lets the program run correctly and does not end it arbitrarily.
The main code where an exception could arise is placed inside the try
block. If an exception occurs, it goes to the catch
block where it is handled; however, the catch
block is skipped if no error is encountered.
The finally
block will always execute in any case, whether an error arises or not in the program.
Below are some code examples of how we can use try..catch..finally
for exception handling in TypeScript.
function doOrThrow<T>(error: T): true{
if (Math.random() > .5){
console.log('true')
return true;
}
else{
throw error;
}
}
try{
doOrThrow('err1');
doOrThrow('err2');
doOrThrow('err3');
}
catch (e:any){
console.log(e,'error')
}
finally{
console.log("Terminated");
}
Output:
The function inside the try
block is called thrice, where it passes an argument. If the generated number is greater than 0.5, it will execute the if
block and return true
.
Otherwise, it will throw an error, which is handled at the catch
block. It tells which call it is thrown after the execution of try
and catch
.
Consider one more example below.
let fun: number; // Notice use of `let` and explicit type annotation
const runTask=()=>Math.random();
try{
fun = runTask();
console.log('Try Block Executed');
throw new Error("Done");
}
catch(e){
console.log("Error",e);
}
finally{
console.log("The Code is finished executing.");
}
Output:
unknown
Type of catch
Clause Variable
Before version 4.0
of TypeScript, the catch
clause variables have a type of any
. Since the variables have the type any
assigned to them, they lack the type-safety, leading to invalid operations and errors.
Now, version 4.0
of TypeScript allows us to specify the type unknown
of the catch
clause variable, which is much safer than the type any
.
It reminds us that we need to perform a type-check of some sort before operating on values.
try {
// ...
}
catch (err: unknown) {
// error!
// property 'toUpperCase' does not exist on 'unknown' type.
console.log(err.toUpperCase());
if (typeof err === "string") {
// works!
// We have narrowed 'err' down to the type 'string'.
console.log(err.toUpperCase());
}
}
Several years ago, the entire booking and check-in system for a major airline in the United States ceased to function for more than an hour during the morning rush on a weekday. This resulted in flight delays for the entire day all across the country. In the end, the cause was found to be an unhanded error that resulted in a flight lookup service to become completely unresponsive.
Handling errors in TypeScript and JavaScript is one of the fundamental things a developer should be experienced in. It’s as important as the rest of the code, and should never be overlooked or underestimated. This is a guide to help newer developers understand how to handle errors, throwing errors, and using try/catch/finally according to industry standards, and also how not to handle them.
Best Practice – Getting Type Information When Catching Errors
Here is a method of getting type information when catching an error:
try { // code that may throw an error... } catch(e) { if(e instanceof Error) { // IDE type hinting now available // properly handle Error e } else if(typeof e === 'string' || e instanceof String) { // IDE type hinting now available // properly handle e or...stop using libraries that throw naked strings } else if(typeof e === 'number' || e instanceof Number) { // IDE type hinting now available // properly handle e or...stop using libraries that throw naked numbers } else if(typeof e === 'boolean' || e instanceof Boolean) { // IDE type hinting now available // properly handle e or...stop using libraries that throw naked booleans } else { // if we can't figure out what what we are dealing with then // probably cannot recover...therefore, rethrow // Note to Self: Rethink my life choices and choose better libraries to use. throw e; } }
As shown above, attempting to find the type of error you are handling can get quite tedious. In most cases, you will know what you kinds of error situations you can recover from. The rest should be re-thrown and never swallowed (more on this later). Here is a much simplified and more typical version:
try { // code that may throw an error... } catch(e) { if(e instanceof Error) { // IDE type hinting now available // properly handle Error e } else { // probably cannot recover...therefore, rethrow throw e; } }
Best Practice – Always Use Error Objects, Never Scalars
When throwing or returning errors, always use instances of the Error object or objects derived from it. Never throw scalars (e.g. number, naked strings, etc). This ensure stack traces are collected and error typing can be used when handling the error.
Here is an example of returning an Error in code utilizing callbacks:
function example1(param: number, callback: (err: Error, result: number) => void) { try { // code that may throw an error... const exampleResult = 1; callback(null, exampleResult); } catch(e) { callback(e, null); // caller handles the error } }
Here is an example of throwing an error in an async function:
async function example2() { await functionReturningPromise(); throw new Error('example error message'); }
Notice how much less code is needed and how it much simpler it appears. This is a common theme when using async/await. I cannot recommend it enough. Back to errors…
Best Practice – Free Resources in Finally
The finally clause should be used to free any resources.
async function example2() { let resource: ExampleResource = null; try { resource = new ExampleResource(); resource.open(); // use the resource... } catch(e) { if(e instanceof Error) { // IDE type hinting now available // handle error if possible // be sure to re-throw it if you can't properly resolve it } else { // probably dealing with a naked string or number here // handle if you can, otherwise re-throw. throw e; } } finally { // always executed // free resources (e.g. close database) as needed if(resource != null) { resource.close(); } } }
Best Practice – Be Specific with Errors
Often you will want to throw or return errors that are more specific than the generic Error object. That is easy to do by extending the standard error object.
class InvalidArgumentError extends Error { constructor(message?: string) { super(message); // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain this.name = InvalidArgumentError.name; // stack traces display correctly now } }
The goal with having a more specific error is to maximize the chance that the error can be handled locally and allow code execution to continue. Here is how you catch the specific errors.
try { throw new InvalidArgumentError(); } catch(e) { if(e instanceof InvalidArgumentError) { // handle InvalidArgumentError } else if(e instanceof Error) { // handle generic Errors // IMPORTANT: list errors from most specific to least specific (child ~> parent) } else { throw e; } }
Errors within the “if” conditions must be listed from most specific to least specific. After all, an InvalidArgumentError is an instance of an Error too.
Best Practice – Never Swallow Exceptions
As far as the browser is concerned as long as the Error is caught, it will happily continue executing your code. In fact, doing something with the Error in the catch block is entirely optional. Swallowing an exception refers to the act of catching an Error and then doing nothing to fix the problem. Here is an example of swallowing an Error.
function exceptionSwallower1() { try { throw new Error('my error'); } catch(e) { /* it's our little secret */ } }
While valid code, it is very bad practice. At a minimum, the Error should be re-thrown.
This is valid and the compiler doesn’t complain, but please don’t ever do this unless you have an extremely well documented reason. While the exception is caught, we do nothing to fix the arising issue and any kind of useful information we could extract from the caught error thrown on the floor and is lost.
Another common and not very helpful practice is to log the Error to the console and continue:
function exceptionSwallower2() { try { throw new Error('my error'); } catch(e) { console.log(err); } }
Errors can also be swallowed by abruptly returning in a finally block:
function exceptionSwallower3() { try { throw new Error('my error'); } finally { return null; } }
First the error is thrown. Then the finally block is executed and the code abruptly returns resulting in all error information being lost. The error is again swallowed.
Errors can also be swallowed by shadowing them:
function exceptionSwallower4() { try { throw new Error('my error'); } finally { throw new Error('different error'); // the first error is now completely hidden } }
Best Practice – Never Use throw as a GoTo
Sometimes, someone will think they are clever by using the try/catch mechanism as way to control code flow when an error condition doesn’t really exist.
function badGoto() { try { // some code 1 if(some_condition) { throw new Error(); } // some code 2 } catch(e) { // some_condition was true // some code 3 (non-error handling code) } }
The end goal of this code example is to skip “some code 2”. This is ineffective and slow due to try/catch being designed for error conditions, not regular program logic flow. JavaScript/TypeScript offers more than enough flow control logic to do just about anything, so simulating a goto statement is not the best idea.
Best Practice – Never Catch for the Purpose of Logging and Then Re-throwing
When trying to figure out why your application is crashing, do not both log the exception and then immediate re-throw it:
function clogMyLogs() { try { throw new Error('my error'); } catch(e) { console.log(err); throw err; } }
Doing both is redundant and will result in multiple log messages that will clog your logs with the amount of text.
@mcclure Personally I don’t think your suggestions are ‘unreliable’ any more or less that other patterns in TypeScript which can also be made unreliable if we try hard enough.
The thing that makes @bali182’s original suggestion likely incompatible with TypeScript’s design goals is that the following three examples only differ by a type annotation, but all would generate different JS code with different runtime behaviour:
try { foo(); } catch (ex) {/***/} try { foo(); } catch (ex: Error) {/***/} try { foo(); } catch (ex: FooError) {/***/}
Type annotations are there to inform the compiler about what the program actually does, but they never change what it does. With these ‘hints’, the type checker can detect invalid usage at compile-time and produce useful early feedback to the developer. If you strip out all the type annotations in a program, that would not change the meaning/behaviour of the program. But it would in the above example.
Your other suggestion (multiple catch (e instanceof X) {...}
blocks) is better in the sense that it doesn’t rely on type annotations. But it’s still a big ask, because it introduces new syntax that has no corresponding ECMA proposal (IFAIK). Apart from adding types to JavaScript, TypeScript has pioneered a few other kinds of new syntax (class properties, modules, namespaces, async/await), but generally only ones that either have an ECMA proposal in development, or represent a very widespread idiom (eg namespaces are just JavaScript’s ubiquitous ‘revealing module pattern’).
But mainly, the problems in this issue would simply vanish if TypeScript just allowed the catch
variable to be narrowed. We already have type guards and control flow analysis. I think there’s little need to add new syntax for this one special case, when everything is already provided to write idiomatic code, if only the catch variable could be narrowed:
try { foo(); } catch (ex) { if (ex instanceof FooError) { /* recover from FooError */ ex.foo ... } else { throw ex; } }
Thas already works at runtime and compiles without errors too, but is just not typesafe because ex
remains typed as any
thoughout so no typos will be found, and no refactorings will find symbol references in this code.
Alrighty, let’s talk about this:
const reportError = ({message}) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({message: error.message})
}
Good so far? Well, that’s because this is JavaScript. Let’s throw TypeScript at
this:
const reportError = ({message}: {message: string}) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({message: error.message})
}
That reportError
call there isn’t happy. Specifically it’s the error.message
bit. It’s because (as of recently) TypeScript defaults our error
type to
unknown
. Which is truly what it is! In the world of errors, there’s not much
guarantees you can offer about the types of errors that are thrown. In fact,
this is the same reason you can’t provide the type for the .catch(error => {})
of a promise rejection with the promise generic
(Promise<ResolvedValue, NopeYouCantProvideARejectedValueType>
). In fact, it
might not even be an error that’s thrown at all. It could be just about
anything:
throw 'What the!?'
throw 7
throw {wut: 'is this'}
throw null
throw new Promise(() => {})
throw undefined
Seriously, you can throw anything of any type. So that’s easy right? We could
just add a type annotation for the error to say this code will only throw an
error right?
try {
throw new Error('Oh no!')
} catch (error: Error) {
// we'll proceed, but let's report it
reportError({message: error.message})
}
Not so fast! With that you’ll get the following TypeScript compilation error:
Catch clause variable type annotation must be 'any' or 'unknown' if specified. ts(1196)
The reason for this is because even though in our code it looks like there’s no
way anything else could be thrown, JavaScript is kinda funny and so its
perfectly possible for a third party library to do something funky like
monkey-patching the error constructor to throw something different:
Error = function () {
throw 'Flowers'
} as any
So what’s a dev to do? The very best they can! So how about this:
try {
throw new Error('Oh no!')
} catch (error) {
let message = 'Unknown Error'
if (error instanceof Error) message = error.message
// we'll proceed, but let's report it
reportError({message})
}
There we go! Now TypeScript isn’t yelling at us and more importantly we’re
handling the cases where it really could be something completely unexpected.
Maybe we could do even better though:
try {
throw new Error('Oh no!')
} catch (error) {
let message
if (error instanceof Error) message = error.message
else message = String(error)
// we'll proceed, but let's report it
reportError({message})
}
So here if the error isn’t an actual Error
object, then we’ll just stringify
the error and hopefully that will end up being something useful.
Then we can turn this into a utility for use in all our catch blocks:
function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message
return String(error)
}
const reportError = ({message}: {message: string}) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({message: getErrorMessage(error)})
}
This has been helpful for me in my projects. Hopefully it helps you as well.
Update: Nicolas had
a nice suggestion for
handling situations where the error object you’re dealing with isn’t an actual
error. And then Jesse had
a suggestion
to stringify the error object if possible. So all together the combined
suggestions looks like this:
type ErrorWithMessage = {
message: string
}
function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
)
}
function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
if (isErrorWithMessage(maybeError)) return maybeError
try {
return new Error(JSON.stringify(maybeError))
} catch {
// fallback in case there's an error stringifying the maybeError
// like with circular references for example.
return new Error(String(maybeError))
}
}
function getErrorMessage(error: unknown) {
return toErrorWithMessage(error).message
}
Handy!
Conclusion
I think the key takeaway here is to remember that while TypeScript has its funny
bits, don’t dismiss a compilation error or warning from TypeScript just because
you think it’s impossible or whatever. Most of the time it absolutely is
possible for the unexpected to happen and TypeScript does a pretty good job of
forcing you to handle those unlikely cases… And you’ll probably find they’re
not as unlikely as you think.