Designing Resilient TypeScript Systems: Handling Unexpected Runtime Exceptions Gracefully
Learn how to build resilient TypeScript applications by effectively handling unexpected runtime exceptions to improve stability and user experience.
When building TypeScript applications, it's common to think that static types solve all problems. However, even with TypeScript's strong typing, unexpected runtime exceptions can still occur—for example, when working with external APIs, user inputs, or asynchronous code. Designing your system to gracefully handle these exceptions ensures your application remains stable and provides a better user experience.
In this article, we'll explore some beginner-friendly strategies to handle unexpected runtime exceptions in TypeScript, including try-catch blocks, error types, and global error handling.
1. Use try-catch Blocks to Handle Exceptions Locally
The simplest and most direct way to handle runtime errors is with try-catch blocks. This approach lets you catch errors where they happen and decide how to respond.
function parseJson(jsonString: string) {
try {
const result = JSON.parse(jsonString);
console.log("Parsed result:", result);
} catch (error) {
console.error("Could not parse JSON:", error);
}
}
parseJson('{"valid": true}');
parseJson('invalid json');2. Use Custom Error Types for More Meaningful Error Handling
You can define custom error classes extending the built-in Error type. This helps you identify and react differently to various error scenarios.
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
function fetchData(success: boolean) {
if (!success) {
throw new NetworkError("Failed to fetch data");
}
return { data: "Some data" };
}
try {
const data = fetchData(false);
console.log(data);
} catch (error) {
if (error instanceof NetworkError) {
console.error("Network issue:", error.message);
} else {
console.error("Unknown error:", error);
}
}3. Handle Errors in Asynchronous Code Using try-catch with async/await
When working with asynchronous code, wrap your await calls in try-catch blocks.
async function getUserData(userId: string) {
try {
const response = await fetch(`https://api.example.com/user/${userId}`);
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
const data = await response.json();
console.log("User data:", data);
} catch (error) {
console.error("Error fetching user data:", error);
}
}
getUserData("123");4. Use Global Error Handlers for Catching Unhandled Exceptions
In some environments like Node.js or browsers, you can register global error handlers to catch uncaught exceptions and unhandled promise rejections.
// Node.js example
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Ideally do some cleanup and restart or shutdown gracefully
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
});5. Provide User-Friendly Error Messages and Recovery Options
Whenever possible, show helpful error messages to users instead of generic failures. Consider logging errors for developers and allowing your app to recover or retry.
To summarize, even though TypeScript helps reduce many runtime bugs through static typing, gracefully handling runtime exceptions is essential to build resilient applications. Use try-catch blocks, custom error types, async-await error handling, and global error listeners to create robust systems that can recover from unexpected situations.