Skip to main content

effective-error-handling

Effective Error Handling in TypeScript:

Understanding Errors:

Know the difference between compile-time errors, which TypeScript catches by analyzing your code, and runtime errors, which occur while the application is running.

// Compile-time error example
let foo: number = 'bar'; // Error: Type 'string' is not assignable to type 'number'.

// Runtime error example
JSON.parse("not valid JSON"); // Throws a SyntaxError at runtime

The Role of Types:

Utilize TypeScript’s static typing to prevent errors before they happen. The stronger your types, the less chance there is for unexpected behavior.

function addNumbers(a: number, b: number): number {
return a + b;
}

addNumbers(10, 5); // No error
addNumbers('10', '5'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Using 'try...catch':

Familiarize yourself with the 'try...catch' statement to handle errors gracefully. Wrap code that might throw an error in a 'try' block and handle the error in the 'catch' block.

try {
throw new Error("Oops, something went wrong!");
} catch (error) {
console.error(error); // Error handling
}

Throwing Errors:

Understand when to throw errors. Create custom error classes and use them to throw errors that are meaningful and specific to your application's logic.

class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}

function validateEmail(email: string) {
if (!email.includes('@')) {
throw new ValidationError("This is not a valid email.");
}
}

try {
validateEmail('testexample.com');
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.message); // Custom error handling
}
}

Error Boundaries:

Consider setting up error boundaries, which are components or functions that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI.

This concept is more relevant to frameworks like React. TypeScript doesn't have built-in support for error boundaries, but it supports the implementation through type-checking.

// This example would be implemented in a React component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

The 'finally' Clause:

Remember to use the 'finally' clause. It runs after the 'try' and 'catch' blocks have executed, which is great for cleanup activities, like closing resources regardless of the outcome.

function handleFile() {
let fileHandle;
try {
fileHandle = openFile("config.json");
// Process the file
} catch (error) {
console.error("An error occurred:", error);
} finally {
fileHandle?.close(); // Always close the file handle, even if an error occurred
}
}

Checking for Null and Undefined:

Always check for null or undefined values to prevent 'null reference errors'. Use TypeScript's non-null assertion operator or optional chaining where appropriate.

function printMessage(message?: string | null) {
// Optional chaining and nullish coalescing combined
console.log(message?.trim() ?? "No message provided");
}

Type Guards for Runtime Errors:

Implement type guards to check the shape of an unknown object at runtime, ensuring that the object matches the expected type.

interface User {
name: string;
age: number;
}

function isUser(obj: any): obj is User {
return 'name' in obj && 'age' in obj;
}

function greet(user: any) {
if (isUser(user)) {
console.log(`Hello, ${user.name}`);
} else {
throw new Error("Invalid user object");
}
}

Each of these examples highlights an important aspect of error handling in TypeScript, providing strategies for both preventing and dealing with errors in a type-safe way.

User Input Validation:

Validate user input rigorously. If you're expecting a certain type, make sure to validate the input before using it in your application.

function isNumber(value: any): value is number {
return typeof value === 'number' && !isNaN(value);
}

function processInput(input: any) {
if (!isNumber(input)) {
throw new Error('Input must be a number.');
}
// Proceed with the assumption that input is a number
}

Asynchronous Error Handling:

For asynchronous operations, use '.catch()' with promises or try/catch with async/await to handle errors that may occur during these operations.

async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Use data
} catch (error: unknown) {
// Handle error
console.error('Fetching data failed', error);
}
}

Error Logging:

Log errors effectively. Use logging libraries or services to record errors, which can help with debugging and monitoring the health of your application.

function logError(error: Error) {
console.error(error); // Replace with more sophisticated error logging mechanism
// Consider using a service like Sentry, LogRocket, or your own custom logging solution
}

Custom Error Handling Functions:

Write custom error handling functions that can encapsulate repetitive error handling logic, making your try/catch blocks cleaner and more reusable.

function handleError(error: unknown) {
// Custom logic for handling different types of errors
if (error instanceof ValidationError) {
// Handle validation error
} else if (error instanceof SyntaxError) {
// Handle syntax error
} else {
// Handle unknown errors
logError(error as Error);
}
}

The 'unknown' Type:

When catching errors, type the error as 'unknown'. This is a safer alternative to typing it as 'any', as it forces you to perform type checking before you operate on the error object.

try {
// Code that may throw an error
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('An unexpected error occurred');
}
}

Error Propagation:

Understand error propagation. Decide whether an error should be handled locally or propagated up to the calling code.

function mightFail() {
if (Math.random() > 0.5) {
throw new Error('Failed!');
}
return 'Success!';
}

function caller() {
try {
const result = mightFail();
console.log(result);
} catch (error) {
// Decide whether to handle the error or propagate it
throw error; // Propagate the error
}
}

User-Friendly Error Messages:

Always remember the end-user. Translate technical error messages into friendly and helpful feedback for the user, without exposing sensitive information.

function handleLoginError(error: Error) {
switch (error.message) {
case 'UserNotFound':
return 'The username you entered does not exist.';
case 'WrongPassword':
return 'The password you entered is incorrect.';
default:
return 'An unexpected error occurred. Please try again later.';
}
}

These examples show different aspects of error handling, from validation to logging, and user feedback, aiming for robustness and a good user experience in TypeScript applications.