Error handling is a crucial aspect of software development, especially in TypeScript, where type safety can create a false sense of security. While TypeScript provides static typing, errors can still occur at runtime due to external APIs, unexpected user input, or issues beyond type checking.
In this article, we’ll explore advanced error handling in TypeScript at runtime, covering best practices, common pitfalls, how to write meaningful error messages and fallbacks, and the security concerns in public error logging.
Understanding Error Types in TypeScript
Before diving into advanced techniques, it's essential to understand the different types of errors in TypeScript:
a) Syntax Errors
Handled by TypeScript’s compiler (e.g., missing semicolons, incorrect syntax). Not a runtime concern.
b) Type Errors
TypeScript helps catch type mismatches at compile-time, but not at runtime.
c) Runtime Errors
These occur despite TypeScript’s static checks, often due to:
- Null or undefined values (
Cannot read property 'x' of undefined
) - External API failures (e.g., network failures, malformed responses)
- Logic errors (e.g., infinite loops, division by zero)
- Unhandled Promises (async errors)
Best Practices for Runtime Error Handling in TypeScript
a) Always Use try/catch
for Critical Code
While TypeScript can enforce types at compile-time, you must handle unexpected runtime issues.
Example: Handling API Calls
async function fetchData(url: string): Promise<any> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching data:", error);
return null; // Return a fallback value
}
}
Best Practice:
- Always wrap critical operations (e.g., API calls, database queries) in a
try/catch
block.
b) Use Custom Error Classes for Better Error Categorization
Creating custom error classes helps differentiate between different error types.
Example: Custom Errors
class NotFoundError extends Error {
constructor(public details?: any) {
super("Resource not found");
this.name = "NotFoundError";
}
}
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = "ValidationError";
}
}
function getUser(id: number) {
if (id <= 0) {
throw new ValidationError("User ID must be positive", "id");
}
// Simulate a missing user
throw new NotFoundError({ userId: id });
}
try {
getUser(-1);
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation Error: ${error.message} (Field: ${error.field})`);
return;
}
if (error instanceof NotFoundError) {
console.error(`Not Found: ${error.message} (Details: ${JSON.stringify(error.details)})`);
return;
}
console.error("Unknown Error:", error);
}
Best Practice:
- Use specific error types (
NotFoundError
,ValidationError
) instead of genericError
.
Writing Meaningful Error Messages and Fallbacks
One of the most overlooked aspects of error handling is how to write effective error messages and fallbacks. Poorly structured error messages can cause frustration for both developers and end users.
a) Characteristics of a Good Error Message
A well-crafted error message should:
- Clearly describe what went wrong (avoid generic messages like "Something went wrong").
- Explain why it happened (if possible).
- Suggest a resolution or alternative action.
- Include context information (such as module, input, or failed operation).
Example: Bad vs. Good Error Messages
❌ Bad Error Message
typescriptCopyEditthrow new Error("Error processing request"
);
- Lacks specificity.
✅ Good Error Message
throw new Error(`Failed to process request: Invalid input in 'email' field`);
- Provides precise details.
Security Concerns in Public Error Logging
Logging errors is crucial for monitoring and debugging, but exposing too much information in logs—especially in public-facing applications—can introduce serious security risks. Attackers can use improperly logged errors to extract sensitive data, analyze system weaknesses, and exploit vulnerabilities.
a) Information You Should Avoid in Public Logs
To prevent security risks, avoid logging sensitive information. Below are key categories of high-risk information that should never be exposed in public logs.
1. Personally Identifiable Information (PII)
Personally Identifiable Information (PII) includes any data that could be used to identify an individual. Exposing PII in logs can lead to privacy breaches and regulatory non-compliance (e.g., GDPR, CCPA).
✅ Safe Logging Example
console.error("User authentication failed due to incorrect credentials.");
❌ Unsafe Logging Example
console.error("User authentication failed for user john.doe@example.com.");
- ❌ Leaks the user's email address in logs.
- ✅ Avoid logging PII like names, emails, phone numbers, and addresses.
2. Authentication Credentials
Never log authentication details such as passwords, API keys, JWT tokens, OAuth tokens, or session identifiers. If these details get exposed, attackers could gain unauthorized access to your system.
✅ Safe Logging Example
console.error("Authentication failed due to invalid credentials.");
❌ Unsafe Logging Example
console.error("Authentication failed. Password entered: secret123.");
- ❌ Exposes a user's password.
- ✅ Remove or mask authentication details in logs.
3. Database Connection Details
Database connection errors can sometimes reveal hostnames, usernames, passwords, or database structures. An attacker could use this information for SQL injection attacks or unauthorized database access.
✅ Safe Logging Example
console.error("Database connection failed.");
❌ Unsafe Logging Example
console.error("Database connection failed: user=admin, password=superSecret.");
- ❌ Reveals database credentials.
- ✅ Log only generic database connection issues.
4. Internal Server Errors & Stack Traces
Exposing stack traces in error messages can leak internal implementation details, including file paths, function names, and dependencies. This information could help attackers identify vulnerabilities in your code.
✅ Safe Logging Example
console.error("An unexpected error occurred. Please contact support.");
❌ Unsafe Logging Example
console.error("TypeError: Cannot read properties of undefined at /src/auth.ts:24:10.");
- ❌ Reveals internal file paths and code structure.
- ✅ In production, replace detailed stack traces with generic messages.
5. API Keys and Internal System Identifiers
Exposing API keys or system identifiers can allow attackers to impersonate legitimate services, leading to unauthorized access and data breaches.
✅ Safe Logging Example
console.error("Error: Invalid API key provided.");
❌ Unsafe Logging Example
console.error("Error: Invalid API key 12345-ABCDE.");
- ❌ Reveals a real API key that could be exploited.
- ✅ Mask API keys in error logs.
6. User IP Addresses & Geolocation Data
Logging IP addresses and geolocation data can expose user behavior and physical locations, which is a privacy concern and a potential security risk.
✅ Safe Logging Example
console.error("Login attempt failed.");
❌ Unsafe Logging Example
console.error("Failed login attempt from IP: 192.168.1.1.");
- ❌ Logs a user’s IP address, which can be used for tracking or attacks.
- ✅ Avoid logging unnecessary location details.
b) Secure Logging Practices
To prevent these issues, follow these secure logging best practices:
1. Use Log Masking and Redaction
Mask or redact sensitive information before logging.
function maskSensitiveData(message: string): string {
return message.replace(/(password|api_key)=[^\s]+/gi, "$1=***");
}
2. Use Different Logging Levels
Differentiate between logs intended for debugging (internal) and those shown in production.
if (process.env.NODE_ENV === "production") {
console.error("An error occurred. Please contact support.");
} else {
console.error("Debug info:", error);
}
- ✅ In development, provide detailed logs.
- ✅ In production, avoid exposing internal details.
3. Implement Centralized Logging with Secure Storage
Instead of exposing logs directly in public consoles, use a centralized logging service like:
- Datadog
- Loggly
- Winston (Node.js logging library)
- AWS CloudWatch / Azure Monitor
Example using Winston for structured logging:
import winston from "winston";
const logger = winston.createLogger({
level: "error",
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "errors.log" }), // Store logs securely
],
});
logger.error("An unexpected error occurred.");
- ✅ Centralized logs allow monitoring without exposing them publicly.
4. Sanitize API Error Responses
If you're building an API, never return raw error messages to clients.
❌ Bad API Error Response
{
"error": "SQL Error: Table 'users' not found at /src/db.ts:45"
}
- ❌ Exposes internal table structure.
✅ Safe API Error Response
{
"error": "An unexpected error occurred. Please try again later."
}
- ✅ Keeps internal details hidden.
Conclusion
Advanced error handling in TypeScript requires careful planning, structured error messages, and meaningful fallbacks. However, security must always be a top priority when logging errors. Exposing internal details in logs or API responses can lead to data leaks and security vulnerabilities.
By following best practices — such as custom error classes, structured logging, sanitizing error outputs, and proper fallback mechanisms—you can build more secure, resilient, and maintainable applications.
Discussion