Throw new error typescript

:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹 - typescript-book/exceptions.md at master · basarat/typescript-book

Exception Handling

JavaScript has an Error class that you can use for exceptions. You throw an error with the throw keyword. You can catch it with a try / catch block pair e.g.

try {
  throw new Error('Something bad happened');
}
catch(e) {
  console.log(e);
}

Error Sub Types

Beyond the built in Error class there are a few additional built-in error classes that inherit from Error that the JavaScript runtime can throw:

RangeError

Creates an instance representing an error that occurs when a numeric variable or parameter is outside of its valid range.

// Call console with too many arguments
console.log.apply(console, new Array(1000000000)); // RangeError: Invalid array length

ReferenceError

Creates an instance representing an error that occurs when de-referencing an invalid reference. e.g.

'use strict';
console.log(notValidVar); // ReferenceError: notValidVar is not defined

SyntaxError

Creates an instance representing a syntax error that occurs while parsing code that isn’t valid JavaScript.

1***3; // SyntaxError: Unexpected token *

TypeError

Creates an instance representing an error that occurs when a variable or parameter is not of a valid type.

('1.2').toPrecision(1); // TypeError: '1.2'.toPrecision is not a function

URIError

Creates an instance representing an error that occurs when encodeURI() or decodeURI() are passed invalid parameters.

decodeURI('%'); // URIError: URI malformed

Always use Error

Beginner JavaScript developers sometimes just throw raw strings e.g.

try {
  throw 'Something bad happened';
}
catch(e) {
  console.log(e);
}

Don’t do that. The fundamental benefit of Error objects is that they automatically keep track of where they were built and originated with the stack property.

Raw strings result in a very painful debugging experience and complicate error analysis from logs.

You don’t have to throw an error

It is okay to pass an Error object around. This is conventional in Node.js callback style code which takes callbacks with the first argument as an error object.

function myFunction (callback: (e?: Error)) {
  doSomethingAsync(function () {
    if (somethingWrong) {
      callback(new Error('This is my error'))
    } else {
      callback();
    }
  });
}

Exceptional cases

Exceptions should be exceptional is a common saying in computer science. There are a few reasons why this is true for JavaScript (and TypeScript) as well.

Unclear where it is thrown

Consider the following piece of code:

try {
  const foo = runTask1();
  const bar = runTask2();
}
catch(e) {
  console.log('Error:', e);
}

The next developer cannot know which function might throw the error. The person reviewing the code cannot know without reading the code for task1 / task2 and other functions they might call etc.

Makes graceful handling hard

You can try to make it graceful with explicit catch around each thing that might throw:

try {
  const foo = runTask1();
}
catch(e) {
  console.log('Error:', e);
}
try {
  const bar = runTask2();
}
catch(e) {
  console.log('Error:', e);
}

But now if you need to pass stuff from the first task to the second one the code becomes messy: (notice foo mutation requiring let + explicit need for annotating it because it cannot be inferred from the return of runTask1):

let foo: number; // Notice use of `let` and explicit type annotation
try {
  foo = runTask1();
}
catch(e) {
  console.log('Error:', e);
}
try {
  const bar = runTask2(foo);
}
catch(e) {
  console.log('Error:', e);
}

Not well represented in the type system

Consider the function:

function validate(value: number) {
  if (value < 0 || value > 100) throw new Error('Invalid value');
}

Using Error for such cases is a bad idea as it is not represented in the type definition for the validate function (which is (value:number) => void). Instead a better way to create a validate method would be:

function validate(value: number): {error?: string} {
  if (value < 0 || value > 100) return {error:'Invalid value'};
}

And now its represented in the type system.

Unless you want to handle the error in a very generic (simple / catch-all etc) way, don’t throw an error.

Обработка исключений¶

В JavaScript есть класс Error, который можно использовать для исключений. Вы выбрасываете ошибку с ключевым словом throw. Вы можете отловить её с помощью блоков try / catch, например:

try {
    throw new Error('Случилось что-то плохое');
} catch (e) {
    console.log(e);
}

Подтипы ошибок¶

Помимо встроенного класса Error, существует несколько дополнительных встроенных классов ошибок, которые наследуются от Error, которые может генерировать среда выполнения JavaScript:

RangeError¶

Создается экземпляр ошибки, которая возникает, когда числовая переменная или параметр выходит за пределы допустимого диапазона.

// Вызов консоли с слишком большим количеством параметров
console.log.apply(console, new Array(1000000000));
// RangeError: Невалидная длина массива

ReferenceError¶

Создается экземпляр ошибки, которая возникает при разыменовании недействительной ссылки. Например:

'use strict';
console.log(notValidVar); // ReferenceError: notValidVar не определена

SyntaxError¶

Создается экземпляр ошибки, возникающей при синтаксическом анализе кода, который не является допустимым в JavaScript.

1***3; // SyntaxError: Непредвиденный токен *

TypeError¶

Создается экземпляр ошибки, которая возникает, когда переменная или параметр имеет недопустимый тип.

'1.2'.toPrecision(1); // TypeError: '1.2'.toPrecision не является функцией

URIError¶

Создается экземпляр ошибки, которая возникает, когда в encodeURI() или decodeURI() передаются недопустимые параметры.

decodeURI('%'); // URIError: URI неправильно сформирован

Всегда используйте Error

Начинающие разработчики JavaScript иногда просто бросают необработанные строки, например.

try {
    throw 'Случилось что-то плохое';
} catch (e) {
    console.log(e);
}

Не делайте так. Основное преимущество объектов Error состоит в том, что автоматически отслеживается где они были созданы и произошли с помощью свойства stack.

Необработанные строки приводят к очень болезненной отладке и затрудняют анализ ошибок из логов.

Вам не нужно выбрасывать ошибку¶

Это нормально передавать объект Error. Это общепринятый код в Node.js колбэк стиле, который принимает колбэк первым параметром как объект ошибки.

function myFunction (callback: (e?: Error)) {
  doSomethingAsync(function () {
    if (somethingWrong) {
      callback(new Error('Это моя ошибка'))
    } else {
      callback();
    }
  });
}

Исключительные случаи¶

Исключения должны быть исключительными — это частая поговорка в компьютерных науках. Это одинаково справедливо и для JavaScript (и для TypeScript) по нескольким причинам.

Неясно откуда брошено исключение¶

Рассмотрим следующий фрагмент кода:

try {
    const foo = runTask1();
    const bar = runTask2();
} catch (e) {
    console.log('Ошибка:', e);
}

Следующий разработчик не знает, какая функция может вызвать ошибку. Человек, просматривающий код, не может знать об этом, не прочитав код для task1 / task2 и других функций, которые они могут вызвать внутри себя и т.д.

Делает поэтапную обработку сложной¶

Вы можете попытаться сделать обработку поэтапной с помощью явного отлова вокруг каждого места, которое может бросить ошибку:

try {
    const foo = runTask1();
} catch (e) {
    console.log('Ошибка:', e);
}
try {
    const bar = runTask2();
} catch (e) {
    console.log('Ошибка:', e);
}

Но теперь, если вам нужно передать что-то из первой задачи во вторую, код становится грязным: (обратите внимание на мутацию foo, требующую let + явную необходимость описывать ее, потому что это не может быть логически выведено от возврата runTask1):

let foo: number; // Обратите внимание на использование `let`
// и явное описание типа
try {
    foo = runTask1();
} catch (e) {
    console.log('Ошибка:', e);
}
try {
    const bar = runTask2(foo);
} catch (e) {
    console.log('Ошибка:', e);
}

Не очень хорошо отражено в системе типов¶

Рассмотрим функцию:

function validate(value: number) {
    if (value < 0 || value > 100)
        throw new Error('Невалидное значение');
}

Использование Error для таких случаев — плохая идея, так как ошибка не отражена в определении типа для проверки функции (value:number) => void. Вместо этого лучший способ создать метод проверки:

function validate(value: number): { error?: string } {
    if (value < 0 || value > 100)
        return { error: 'Невалидное значение' };
}

И теперь это отражено в системе типов.

Если вы не хотите обрабатывать ошибку очень общим (простым / универсальным и т.д.) способом, не бросайте ошибку.

JavaScript has an Error class that you can use for exceptions. You throw an error with the throw keyword. You can catch it with a try / catch block pair e.g.

throw new Error(‘Something bad happened’);

Beyond the built in Error class there are a few additional built-in error classes that inherit from Error that the JavaScript runtime can throw:

Creates an instance representing an error that occurs when a numeric variable or parameter is outside of its valid range.

// Call console with too many arguments

console.log.apply(console, new Array(1000000000)); // RangeError: Invalid array length

Creates an instance representing an error that occurs when de-referencing an invalid reference. e.g.

console.log(notValidVar); // ReferenceError: notValidVar is not defined

Creates an instance representing a syntax error that occurs while parsing code that isn’t valid JavaScript.

1***3; // SyntaxError: Unexpected token *

Creates an instance representing an error that occurs when a variable or parameter is not of a valid type.

(‘1.2’).toPrecision(1); // TypeError: ‘1.2’.toPrecision is not a function

Creates an instance representing an error that occurs when encodeURI() or decodeURI() are passed invalid parameters.

decodeURI(‘%’); // URIError: URI malformed

Beginner JavaScript developers sometimes just throw raw strings e.g.

throw ‘Something bad happened’;

Don’t do that. The fundamental benefit of Error objects is that they automatically keep track of where they were built and originated with the stack property.

Raw strings result in a very painful debugging experience and complicate error analysis from logs.

You don’t have to throw an error

It is okay to pass an Error object around. This is conventional in Node.js callback style code which takes callbacks with the first argument as an error object.

function myFunction (callback: (e?: Error)) {

doSomethingAsync(function () {

callback(new Error(‘This is my error’))

Exceptions should be exceptional is a common saying in computer science. There are a few reasons why this is true for JavaScript (and TypeScript) as well.

Unclear where it is thrown

Consider the following piece of code:

console.log(‘Error:’, e);

The next developer cannot know which function might throw the error. The person reviewing the code cannot know without reading the code for task1 / task2 and other functions they might call etc.

Makes graceful handling hard

You can try to make it graceful with explicit catch around each thing that might throw:

console.log(‘Error:’, e);

console.log(‘Error:’, e);

But now if you need to pass stuff from the first task to the second one the code becomes messy: (notice foo mutation requiring let + explicit need for annotating it because it cannot be inferred from the return of runTask1):

let foo: number; // Notice use of `let` and explicit type annotation

console.log(‘Error:’, e);

const bar = runTask2(foo);

console.log(‘Error:’, e);

Not well represented in the type system

function validate(value: number) {

if (value < 0 || value > 100) throw new Error(‘Invalid value’);

Using Error for such cases is a bad idea as it is not represented in the type definition for the validate function (which is (value:number) => void). Instead a better way to create a validate method would be:

function validate(value: number): {error?: string} {

if (value < 0 || value > 100) return {error:‘Invalid value’};

And now its represented in the type system.

Unless you want to handle the error in a very generic (simple / catch-all etc) way, don’t throw an error.

Время прочтения
9 мин

Просмотры 8.2K

Заключительная часть статей, посвященных тому, как можно использовать принципы чистого кода в TypeScript(ps. Все эти принципы относятся не только к языку TypeScript).

Тестирование

Тестирование важнее деплоя. Если у вас нет тестов или их мало, то каждый раз при выкладке кода на боевые сервера у вас не будет уверенности, что ничего не сломается. Решение о достаточном количестве тестов остается на совести вашей команды, но 100% покрытие тестами всех выражений и ветвлений обеспечивает высокое доверие к вашему коду и спокойствие всех разработчиков. Из этого следует, что в дополнение к отличному фреймворку для тестирования, необходимо также использовать хороший инструмент покрытия.

Нет никакого оправдания, чтобы не писать тесты. Есть много хороших фреймворков для тестирования на JS с поддержкой типов для TypeScript, так что вы найдите тот который понравится вашей команде. Когда вы найдете тот, который работает для вашей команды, тогда стремитесь всегда писать тесты для каждой новой фичи/модуля, которую вы пишете. Если вы предпочитаете метод тест-ориентированной разработки (TDD), это замечательно, но главное — просто убедиться, что вы достигли своих целей покрытия, прежде чем запускать какую-либо функцию или реорганизовать существующую.

Три закона TDD

  1. Новый рабочий код пишется только после того, как будет написан модульный тест, который не проходит.
  2. Вы пишете ровно такой объем кода модульного теста, какой необходим для того, чтобы этот тест не проходил (если код теста не компилируется, считается, что он не проходит).
  3. Вы пишете ровно такой объем рабочего кода, какой необходим для прохождения модульного теста, который в данный момент не проходит.

Правила F.I.R.S.T.

Чистые тесты должны следовать правилам:

  • Быстрота(Fast) Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.
  • Независимость(Independent) Тесты не должны зависеть друг от друга. Они должны обеспечивать одинаковые выходные данные независимо от того, выполняются ли они независимо или все вместе в любом порядке.
  • Повторяемость(Repeatable) Тесты должны выполняться в любой среде, и не должно быть никаких оправданий тому, почему они провалились.
  • Очевидность(Self-Validating) Тест должен отвечать либо Passed, либо Failed. Вам не нужно сравнивать файлы логов, для чтобы ответить, что тест пройден.
  • Своевременность(Timely) Юнит тесты должны быть написаны перед производственным кодом. Если вы пишете тесты после производственного кода, то вам может показаться, что писать тесты слишком сложно.

Один кейс на тест

Тесты также должны соответствовать Принципу единой ответственности(SPP). Делайте только одно утверждение за единицу теста.(ps. не пренебрегайте этим правилом)

import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles date boundaries', () => {
    let date: AwesomeDate;

    date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));

    date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));

    date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

Хорошо:

import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles 30-day months', () => {
    const date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));
  });

  it('handles leap year', () => {
    const date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));
  });

  it('handles non-leap year', () => {
    const date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

Асинхронность

Используйте promises а не callbacks

Callback-функции ухудшают читаемость и приводят к чрезмерному количеству вложенности (ад обратных вызовов(callback hell)). Существуют утилиты, которые преобразуют существующие функции, используя стиль callback-ов, в версию, которая возвращает промисы (для Node.js смотрите util.promisify, для общего назначения смотрите pify, es6-promisify)

Плохо:

import { get } from 'request';
import { writeFile } from 'fs';

function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
  get(url, (error, response) => {
    if (error) {
      callback(error);
    } else {
      writeFile(saveTo, response.body, (error) => {
        if (error) {
          callback(error);
        } else {
          callback(null, response.body);
        }
      });
    }
  });
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
  if (error) {
    console.error(error);
  } else {
    console.log(content);
  }
});

Хорошо:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);

function downloadPage(url: string, saveTo: string): Promise<string> {
  return get(url)
    .then(response => write(saveTo, response));
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
  .then(content => console.log(content))
  .catch(error => console.error(error));  

Промисы поддерживают несколько вспомогательных методов, которые помогают сделать код более понятным:

Promise.all особенно полезен, когда есть необходимость запускать задачи параллельно. Promise.race облегчает реализацию таких вещей, как тайм-ауты для промисов.

Обработка ошибок

Бросать ошибки — хорошее решение! Это означает, что во время выполнения вы будете знать, если что-то пошло не так, вы сможете остановить выполнение вашего приложения убив процесс (в Node) в нужный момент и увидеть место ошибки с помощью стек трейса в консоли.

Всегда используйте ошибки для отклонений(reject)

JavaScript и TypeScript позволяют вам делать throw любым объектом. Промис также может быть отклонен с любым объектом причины. Рекомендуется использовать синтаксис throw с типом Error. Это потому что ваша ошибка может быть поймана в более высоком уровне кода с синтаксисом catch. Было бы очень странно поймать там строковое сообщение и сделать отладку более болезненной. По той же причине вы должны отклонять промисы с типами Error.

Плохо:

function calculateTotal(items: Item[]): number {
  throw 'Not implemented.';
}

function get(): Promise<Item[]> {
  return Promise.reject('Not implemented.');
}

Хорошо:

function calculateTotal(items: Item[]): number {
  throw new Error('Not implemented.');
}

function get(): Promise<Item[]> {
  return Promise.reject(new Error('Not implemented.'));
}

// or equivalent to:

async function get(): Promise<Item[]> {
  throw new Error('Not implemented.');
}

Преимущество использования типов Error заключается в том, что они поддерживается синтаксисом try/catch/finally и неявно всеми ошибками и имеют свойство stack, которое является очень мощным для отладки. Есть и другие альтернативы: не использовать синтаксис throw и вместо этого всегда возвращать пользовательские объекты ошибок. TypeScript делает это еще проще.
Рассмотрим следующий пример:

type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;

function calculateTotal(items: Item[]): Failable<number, 'empty'> {
  if (items.length === 0) {
    return { isError: true, error: 'empty' };
  }

  // ...
  return { isError: false, value: 42 };
}

Для подробного объяснения этой идеи обратитесь к оригинальному посту.

Не игнорируйте отловленные ошибки

Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль (console.log) не намного лучше, так как зачастую оно может потеряться в море консольных записей. Оборачивание куска кода в try/catch означает, что вы предполагаете возможность появления ошибки и имеете на этот случай четкий план.

Плохо:

try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

// or even worse

try {
  functionThatMightThrow();
} catch (error) {
  // ignore error
}

Хорошо:

import { logger } from './logging'

try {
  functionThatMightThrow();
} catch (error) {
  logger.log(error);
}

Не игнорируйте ошибки, возникшие в промисах

Вы не должны игнорировать ошибки в промисах по той же причине, что и в try/catch.

Плохо:

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    console.log(error);
  });

Хорошо:

import { logger } from './logging'

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    logger.log(error);
  });

// or using the async/await syntax:

try {
  const user = await getUser();
  await sendEmail(user.email, 'Welcome!');
} catch (error) {
  logger.log(error);
}

Форматирование

Форматирование носит субъективный характер. Как и во многом собранном здесь, в вопросе форматирования нет жестких правил, которым вы обязаны следовать. Главное — НЕ СПОРИТЬ по поводу форматирования. Есть множество инструментов для автоматизации этого. Используйте один! Это трата времени и денег когда инженеры спорят о форматировании. Общее правило, которому стоит следовать соблюдайте правила форматирования принятые в команде

Для TypeScript есть мощный инструмент под названием TSLint. Это статический анализ инструмент, который может помочь вам значительно улучшить читаемость и поддерживаемость вашего кода. Но лучще используйте ESLint, так как TSLint больше не поддерживается.
Есть готовые к использованию конфигурации TSLint и ESLint, на которые вы можете ссылаться в своих проектах:

  • TSLint Config Standard — стандартный набор правил

  • TSLint Config Airbnb — правила от Airbnb

  • TSLint Clean Code — Правила TSLint которые вдохновлены Clean Code: A Handbook of Agile Software Craftsmanship

  • TSLint react — правила, связанные с React & JSX

  • TSLint + Prettier — правила линта для Prettier средство форматирования кода

  • ESLint rules for TSLint — ESLint правила для TypeScript

  • Immutable — правила отключения мутации в TypeScript

Обратитесь также к этому великому TypeScript StyleGuide and Coding Conventions источнику.

Используйте один вариант именования

Использование заглавных букв говорит вам о ваших переменных, функциях и др… Эти правила субъективны, поэтому ваша команда может выбирать все, что они хотят. Дело в том, что независимо от того, что вы все выберите, просто будьте последовательны.

Плохо:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

type animal = { /* ... */ }
type Container = { /* ... */ }

Хорошо:

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

type Animal = { /* ... */ }
type Container = { /* ... */ }

Предпочитайте использовать PascalCase для имен классов, интерфейсов, типов и пространств имен. Предпочитайте использовать camelCase для переменных, функций и членов класса.

Организация импортов

С помощью простых и понятных операторов импорта вы можете быстро увидеть зависимости текущего кода.
Убедитесь, что вы используете следующие хорошие практики для операторов import:

  • Операторы импорта должны быть в алфавитном порядке и сгруппированы.
  • Неиспользованный импорт должен быть удален.
  • Именованные импорты должны быть в алфавитном порядке (т.е. import {A, B, C} 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';)

Плохо:

import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';

Хорошо:

import 'reflect-metadata';

import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';

import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';

import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';

Используйте typescript алиасы

Создайте более симпатичный импорт, определив пути и свойства baseUrl в разделе compilerOptions в tsconfig.json
Это позволит избежать длинных относительных путей при импорте.

Плохо:

import { UserService } from '../../../services/UserService';

Хорошо:

import { UserService } from '@services/UserService';

// tsconfig.json
...
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@services": ["services/*"]
    }
    ...
  }
...

P.S.

Первая часть
Вторая часть
Полный перевод

In TypeScript, just like in JavaScript, the try catch statement is used to handle errors in your code. It consists of three statements:

  1. The try statement that defines the code to try (or run).
  2. The catch statement that defines the code that handles the errors.
  3. 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

  1. The definition
  2. How to catch a specific error?
  3. How to use try catch with async/await?
  4. What are some best practices?
  5. 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.

  1. The code inside the try statement runs.
  2. 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:

  1. It only works for runtime errors, this means that it will not work for syntax errors.
  2. 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'

typescript try catch

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?

  1. Throw your own custom errors, this will help you with development and debugging.
  2. For large applications, you need to implement remote exception logging with window.onerror. This will help you debug your application.
  3. 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.

typescript try catch

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.

Tim Mouskhelichvili

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.

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.

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.

Понравилась статья? Поделить с друзьями:
  • Throw new error sasl scram server first message client password must be a string
  • Throw new error php
  • Throw new error no sequelize instance passed
  • Throw new error msg
  • Throw new error library dir does not exist libdir