Best Practices for Exception Handling in TS

date
Mar 16, 2021
slug
best-practices-for-exception-handling-in-typescript
status
Published
tags
programming
summary
If, like I did, you also have difficulty understanding error handling then you’re in the right place. After years working with JavaScript, and speaking/working with other engineers, a style of applications layout for handling errors emerged in my own work. And it’s this philosophy for error handling that I want to share with you today.
type
Post
By the end of the article you’ll understand how to structure an application to handle errors effectively, achieve more understanding of the application, deliver better error messages and have an easier time debugging.

Why

  • maintaining, readability, performance
  • enable you to write robust and fault-tolerant programs that can deal with problems continue executing or terminate gracefully
function trySomethingRisky(str) {
        if (!isValid(str)) throw new Error('invalid string!')
        return "success!"
}

function main() {
    try {
        return trySomethingRisky(prompt('enter valid name'))
    } catch (err) {
        if (err instanceof Error) {
            // handle exceptions
        } else {
            // handle errors
        }
    }
}
 
If you're thinking that you don't write this sort of code very often, you're probably not thinking through your failure modes enough.
  • JS doesn’t have a native way to indicate whether a function can throw, if you invoke it. So you cannot lint against it — You must etiher pay this cost earlier in manual code review or later if bug report
  • An innocent fs.readFileSync can call bring down a whole server given the wrong string.
  • Promise calls without a catch in the browser will simply log silent errors ( a terriable user experiences)
The more function and module boundaries you cross, the more you need to think about defensively adding trycatch and handling the gamut of errors that can happen, and the harder it is to trace where errors begin and where they are handled.
 

Errors vs Exceptions

  • Exceptions are expected failures, which we should recover from.
  • Errors are unexpected failures. By definition, we cannot recover elegantly from unexpected failures.
This is no doubt due to the fact that JavaScriptPython, and other languages treat errors and exceptions as synonyms. PHP and Java seem to have this difference baked into the language
 

Exception Handling vs Error Checking

What

  • Errors are unrecoverable, Exceptions are routine.
  • Incorporate your exception-handling strategy into your system from inception. Including effective exception handling after a system has been implemented can be difficult
  • Exception handling provides a standard mechanism for processing errors. This is especially important when working on a project with a large team of programmers.
  • base on unit of work
  • base on how much fulfill of contract
  • A function should throw an exception before the error has an opportunity to occur
  • Catching an exception object by reference eliminates the overhead of copying the object that represents the thrown exception.
  • Associating each type of runtime error with an appropriately named exception object improves program clarity.
notion image

Let’s see

  1. Don’t manage business logic with exceptions. Use conditional statements instead. If a control can be done with if-else statement clearly, don’t use exceptions because it reduces readability and not performance.
  1. Exception names must be clear and meaningful, stating the causes of exceptions.
  1. Throw exceptions for error conditions while implementing a method. E.g: if you return -1, -2,-3 ect. values instead of FileNotFoundExcpetion, that method can not be understand.
  1. Catch specific exceptions instead of the top Exception class. This will bring additional performance, readability and more specific exception handling.
  1. Null control with conditionals is not an alternative to catching NullPointerException. If a method may return null, control it with if-else statement. If a return may throw NullPointerException, catch it.
  1. Try not to re-throw the exception because of the price. But if re-throwing had been a must, re-throw the same exception instead of creating a new exception. This will bring additional performance. You may add additional info in each layer to that exception.
  1. Define your own exception hierarchy by extending current Exception class (e.g. UserException, SystemException and their sub types) and use them. By doing this you can specialize your exceptions and define a reusable module/layer of exceptions.
  1. Use types of exceptions clearly.
    1. 🔴 Fatal: System crash states
    2. 🔴 Error: Lack of requirement.
    3. 🟡Warn: Not an error but error probability.
    4. 🔵 Info: Info for user.
    5. Debug: Info for developer.
  1. Don't absorb exceptions with no logging and operation. Ignoring exceptions will save that moment but will create a chaos for maintainability later.
  1. Don't log the same exception more than once. This will provide clearness of the exception location.
  1. Always clean up resources (opened files etc.) and perform this in "finally" blocks.
  1. Exception handling inside a loop is not recommended for most cases. Surround the loop with exception block instead.
  1. Granularity is very important. One try block must exist for one basic operation. So don't put hundreds of lines in a try-catch statement.
  1. Exception handling is designed to process synchronous errors, which occur when a statement executes, such as out-of-range array subscripts, arithmetic overflow (i.e., a value outside the representable range of values), division by zero, invalid function parameters and unsuccessful memory allocation (due to lack of memory).
  1. Exception handling is not designed to process errors associated with asynchronous events (e.g., disk I/O completions, network message arrivals, mouse clicks and keystrokes), which occur in parallel with, and independent of, the program’s flow of control.
  1. Avoid using exception handling as an alternate form of flow of control. These “additional” exceptions can “get in the way” of genuine error-type exceptions
  1. Performance tip: When no exceptions occur, exception-handling code incurs little or no performance penalty. Thus, programs that implement exception handling operate more efficiently than do programs that intermix error-handling code with program logic.
  1. Functions with common error conditions should return 0 or NULL (or other appropriate values, such as bools) rather than throw exceptions. A program calling such a function can check the return value to determine success or failure of the function call

Event better with

  1. Produce enough documentation for your exception definitions (at least JavaDoc).
  1. Giving a number/code for each different exception message is a good practice for documentation and faster communication.
  1. Handle exceptions at the boundary between layers of your code. For example, if you have DAOs for database access, feel free to throw exceptions with them, but handle the exception only at the services class that begin the transaction and call the DAOs

Control-flow Anti pattern

Unhandled : When an exception is unhandled, if often results in a clueless user experience for the end user as well as the developer.
async getProjectData(projectId: string): ProjectData {
      const query = `SELECT * FROM Project WHERE project_id=${projectId}`;
			return DAL.exec(query);  //May fail due to database issues
    }

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
       const project = await this.getProjectData(projectId); 
       const data = { ...notification, token }; 
       return await messaging(project).send(data); //May fail due to configuration, network, or authentication  
 }
async getProjectData(projectId: string): ProjectData {
	try {
	      const query = `SELECT * FROM Project WHERE project_id=${projectId}`;
				return await DAL.exec(query);  //May fail due to database issues
	 } catch (error) {
	   return Promise.reject(error);
	 }
}

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
        try {
            const project = await this.getProjectData(projectId); 
            const data = { ...notification, token }; 
            return await messaging(project).send(data); //May fail due to configuration, network, or authentication
        } catch (error) {
            return Promise.reject(error);
        }
 }
Catch-all : With catch-all errors, it’s often difficult to quickly detect the original problem. For the same reason, the end users don’t get specific and actionable error messages.
async getProjectData(projectId: string): ProjectData {
	    const query = `SELECT * FROM Project WHERE project_id=${projectId}`;
			return await DAL.exec(query);  //May fail due to database issues
}

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
        try {
            const project = await this.getProjectData(projectId); 
            const data = { ...notification, token }; 
            return await messaging(project).send(data); //May fail due to configuration, network, or authentication
        } catch (error) {
            return Promise.reject(error);
        }
 }
If-else Exceptions Exceptions mean something unexpected took place. If-else is used for logical known code paths. For example, when accepting an API request, invalid input data is often a known logical path. Using exceptions for it will trigger false alarms.
Wrapped Exception. A new exception is raised hiding the original exception. In such cases, if the exception is handled by the caller, critical context information is lost since the orignal stacktrace is no longer available.
async getProjectData(projectId: string): ProjectData {
	    const query = `SELECT * FROM Project WHERE project_id=${projectId}`;
			return await DAL.exec(query);  //May fail due to database issues
}

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
        try {
            const project = await this.getProjectData(projectId); 
            const data = { ...notification, token }; 
            return await messaging(project).send(data); //May fail due to configuration, network, or authentication
        } catch (error) {
            return Promise.reject(error);
        }
 }
Useless Custom Exception. Introducing a new exception type when a pre-defined exception suits just fine.
async getProjectData(projectId: string): ProjectData {
	    const query = `SELECT * FROM Project WHERE project_id=${projectId}`;
			if(!projectId){
				// Could just use pre-defined ArgumentError
			    throw EmptyTextException("Text can't be empty")
			}
			return await DAL.exec(query);  //May fail due to database issues
}

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
        try {
            const project = await this.getProjectData(projectId); 
            const data = { ...notification, token }; 
            return await messaging(project).send(data); //May fail due to configuration, network, or authentication
        } catch (error) {
            return Promise.reject(error);
        }
 }
Leaky Handler: Handling an error without cleaning system resources such as file handles, open network connections, can cause cascading system outage.
Aborting a program component due to an uncaught exception could leave a resource— such as a file stream or an I/O device—in a state in which other programs are unable to acquire the resource. This is known as a resource leak
// Will leak this file handle if read succeeds, but write fails
async getProjectData(projectId: string): ProjectData {
	  	try {
				const blackListFile = readFileSync('./blacklistProjectIds.txt', 'utf-8');
			
				const query = `SELECT * FROM Project WHERE project_id=${projectId.notIncluded(blackListFile)}`;
				await fs.promises.writeFile(join(__dirname, '/queryLog.txt'), projectId, {flag: 'w'});
				
				return await DAL.exec(query);  // May fail due to database issues
			} catch(error: FileNotFoundError | FileSaveError){
				return Promise.reject(error);
			}
}

async sendNotification(projectId: string, token: string, notification: any): Promise<any> {
        try {
            const project = await this.getProjectData(projectId); 
            const data = { ...notification, token }; 
            return await messaging(project).send(data); //May fail due to configuration, network, or authentication
        } catch (error) {
            return Promise.reject(error);
        }
 }
Stack Unwinding: When an exception is thrown but not caught in a particular scope, the function call stack is “unwound,” and an attempt is made to catch the exception in the next outer try…catch block. Unwinding the function call stack means that the function in which the exception was not caught terminates, all local variables in that function are destroyed and control returns to the statement that originally invoked that function.
// Stack unwinding.
// function A throws runtime error
function A() {
	console.log('function A');
	// no try block, stack unwinding occurs, return control to function
	throw Error("runtime error")
}

function B(){
	console.log('B invoked  A');
	A();
}

function C(){
console.log('C invoked B');
B();
}
// demonstrate stack unwinding
function main() {
try{
	console.log('invoked A from main');
	A();
}catch(e){
	console.log(e)
	console.log('Exception handled in main')
}
}

© 2021 - 2024 · Khanh Tran · 顔エンジン · tran_khanh@outlook.com