Server-Side Exception Handling Patterns & Practices
Error Handling is an important and often overlooked concept in development. In this blog you’ll learn different patterns and practices for better error handling, using C#, Node.js, and popular frameworks for both.
What is Error Handling?
Exception handling is a programming concept that deals with how a program responds to and manages unexpected or exceptional situations that may occur during its execution. These exceptional situations are often referred to as exceptions and can include errors, unexpected inputs, or other events that can disrupt the normal flow of a program.
The goal of exception handling is to gracefully handle these situations rather than allowing them to crash the program. This is important for creating robust and reliable software, as it helps prevent unexpected errors from causing catastrophic failures.
Common causes of exceptions:
- Diving by zero
- Stack Overflow
- Extracting the value that does not exist
- Using a property upon a value that is null
- Invalid values used
- Authorization exception, etc.
Variety of Exceptions
At the heart of Exceptions lies the Exception class, which is the main class for handling Exceptions. All other exceptions inherit from this base class. Languages like Java & C# provide a large array of Exception classes, that are split into:
- System Exceptions
- Application Exceptions
And then further divided into:
- IndexOutOfRangeException
- NullReferenceException
- ArgumentException
- OverflowException
- ArithmeticException
- DivideByZeroException, etc.
For almost every scenario you can think of. You can also create your own Exceptions. More on that later.
Using the correct Exception class helps us better the exception of a given scenario, like the one below:
using System;
public class Program
{
public static void Main()
{
try {
int a = 100;
int b = 0;
int division = a / b;
Console.WriteLine($"Result: {division}");
}
catch (DivideByZeroException e)
{
Console.WriteLine("Division by zero is now allowed!");
}
}
}
Throwing Exceptions
In order to throw, the value you’re “throwing” must derive from the Exception base class (System.Exception
namespace in C#). C# Exceptions have multiple constructor overloads that allow you to shape your exception however you like:
// a)
throw new DivideByZeroException();
// b)
throw new DivideByZeroException("Message");
// c)
throw new DivideByZeroException("Message", new Exception());
Meanwhile in JavaScript, you throw using the Error
class:
// a)
throw new Error();
// b)
throw new Error('Message');
Or throw anything you desire:
// seems legit
throw 'Hello World!'
Custom Exceptions
On top of built-in exceptions, you can create custom class exceptions by inheriting the Exception base class.
class MyCustomException : Exception
{
public MyCustomException() { }
}
A custom exception class should have three standard constructors:
- One without parameters
- One with message parameter
- One with a message and inner exception parameter that can be used to chain exceptions
class MyCustomException: Exception
{
MyCustomException(): base() { }
MyCustomException(string message) : base(message) { }
MyCustomException(string message, Exception innerException)
: base(message, innerException) { }
}
You can throw this exception as you’d any other:
throw new MyCustomException();
Exception Propagation
When the exception occurs, the program automatically exists out of the current context (function or a class) and is propagated up the stack trace back to the previous context (caller) until it finds someone who can handle the exception.
You can think of the program execution flow as a downward pyramid.
When the Exception occurs the program moves up the pyramid until it reaches the error handler.
This is important to understand as it helps you organize your codebase of when and how you want to handle exceptions.
If the exception is never handled, then the OS will jump in and terminate the program resulting in the app crashing to your desktop or server shutting down.
Manual Propagation
Sometimes you may want to manually propagate Exceptions to the next handler inline. This is commonly used when you have a global exception handler and you decide to send all the exceptions toward it or want to avoid duplicating the code.
One way you can do that is to avoid writing error handlers entirely.
function foo() {
try {
bar();
} catch (ex) {
console.log(`Exception was thrown: ${ex.message}`);
}
}
function bar() {
throw new Error('Something went wrong!');
}
foo();
// Exception was thrown: Something went wrong!
So if an exception occurs in the bar()
function, the function calling bar()
(foo()
) handles it.
But what if you already have an error handler?
In these scenarios, you want to avoid rethrowing the exceptions and rather forward the existing exception from Try Catch to the next handler.
🔴 Using JavaScript
In JavaScript, you can propagate the exception using the throw keyword.
function foo() {
try {
bar();
} catch (ex) {
// Foo handles the exception from bar()
console.log(`Exception was thrown: ${ex.message}`);
}
}
function bar() {
try {
throw new Error('Something went wrong!');
} catch (ex) {
throw ex; // <-- sends error to foo();
}
}
foo();
// Exception was thrown: Something went wrong!
🔴 Using C#
C# follows similar exception propagation.
public static void Foo() {
try {
Bar();
}
catch (Exception ex)
{
Console.WriteLine($"Exception was thrown: {ex.Message}");
}
}
public static void Bar() {
try {
throw new Exception("Something went wrong!");
}
catch (Exception ex)
{
throw; // or throw ex <-- sends error to Foo();
}
}
// Exception was thrown: Something went wrong!
🔴 Express.js Route Handler
In Express a route handler can act like a middleware by using the third argument in the router function (the next()
function). Calling next()
with an error will propagate the error to the next error handler:
app.get('/', async (req: Request, res: Response, next: NextFunction) => {
try {
const products = await getProducts();
res.send(products);
} catch (e: unknown) {
next(e); // <--- sends error to the error-handler middleware
}
})
The Caveats
Obviously, there are downsides to manual propagation. If you have one place that handles all errors, you can’t accurately see what went wrong in each scenario.
Take this example into account. Both functions throw errors, but only one place to catch them.
function foo() {
try {
bar();
help();
} catch (ex) {
console.log(`Exception was thrown: ${ex.message}`);
}
}
foo();
function bar() {
throw new Error('Bar went wrong!');
}
function help() {
throw new Error('Help went wrong!');
}
Now imagine if tasks are asynchronous or if there are race conditions, or if functions were doing multiple tasks with different points of failure.
It is very hard to tell what exactly went wrong by handling everything at one global error place.
I prefer a combination. Handle smaller errors (like an item not found) in the service directly (bar()
) and more serious exceptions in the global exception handler (foo()
).
Try Catch Pattern
The Try Catch pattern is the most commonly used Error-Handling practice and it’s based on three steps:
- try
- catch
- finally
You write code that might break (throw Error) in the Try
block, the Catch
is reserved for handling the error.
try {
// Do something
} catch (ex) {
// Handle exception
}
You can also have multiple catch blocks to catch different types of errors:
using System;
public class Program
{
public static void Main()
{
try {
int a = 100;
int b = 0;
int division = a / b;
Console.WriteLine($"Result: {division}");
}
catch (DivideByZeroException e)
{
Console.WriteLine("Division by zero is now allowed!");
}
catch (Exception e)
{
Console.WriteLine($"Something went wromg: {e}");
}
}
}
If the code within the Try
block is ok, the program execution flow should never reach the Catch
block. However, if an error appears at any step, the program will exit the Try
block and resume execution within the Catch
.
The ordering of the exceptions matters. The most specific exceptions (DivideByZeroException) should be at the top, and the least specific exceptions (Exception e) should be at the bottom.
Finally
You’ve just learned about two outcomes of the operation, Try
and succeed or fail and go to Catch
. However, there is a third part — the Finally
block that will execute in either way, no matter if the operation succeeds or fails.
This block is optional and often neglected, although it can play a vital role in error handling. Within the Finally
block you can log the outcome of the operation, dispose of any active dependency, or handle the loader as in the example below:
async function uploadFile(file: File) {
try {
console.log('Upload started!');
// display loader on UI
loaderActive = true;
await startUpload(file)
} catch (e) {
console.log('Upload failed!');
} finally {
// hide loader
loaderActive = false;
}
}
The loader is displayed when upload starts and regardless if the operation succeeds or fails, you want to hide the loader. Thus finally fits right in.
Exception Filtering
Exception filtering gives you greater control over what exceptions you can handle.
🔴 In C#, you can make use of the When
statement, to catch an Exception only when a certain criteria is satisfied.
try
{
string blog = null;
if (blog.Equals("AWS Node.js 2024")) {
Console.WriteLine("Can't wait to read it!");
}
}
catch (Exception ex) when (ex is NullReferenceException)
{
Console.WriteLine("Null Ref Exception thrown!");
}
In this example, exception is captured only when is of type NullReferenceException
.
🔴 JavaScript language also allows you to filter the Exception based on the Exception class:
try {
doSomething();
} catch (e) {
if (e instanceof TypeError) {
// statements to handle TypeError exceptions
} else if (e instanceof RangeError) {
// statements to handle RangeError exceptions
} else if (e instanceof EvalError) {
// statements to handle EvalError exceptions
} else {
// statements to handle any unspecified exceptions
logMyErrors(e);
}
}
The Callback Error Handling
This pattern is commonly used in JavaScript/Node.js when handling Asynchronous Exceptions.
When performing an operation, like reading or writing a file or subscribing to an event, the outcome of the operation results in the callback function. The first parameter is always an error that occurred and the second is the successful data response:
fs.writeFile((error, data) => {
if (error) {
// Error path
}
console.log(data) // Success path
})
This pattern is also used when handling errors within a middleware, which we’ll look into a bit later.
The Promise-based Error Handling
Error handling with Promises is pretty straightforward. Promise class exposes the catch()
method used for handling errors produced by the Promise.
new Promise()
.then(data => data.json()) // Success path
.catch(error => console.log(error)) // Error path
But what if you want to delegate that to another service that will handle the error?
One trick I like to use is to return a boolean inside the catch block.
interface FunctionResponse {
data?: any // data produced by the Promise
isError?: boolean //
}
function doSomething(): FunctionResponse {
return new Promise
.then(data => data.json())
.then(data => { data })
.catch(error => { isError: true })
}
Then when you call this function you can validate the error
async function mainFunction {
const response = await doSomething()
if (response.isError) {
// handle Error
}
}
To keep things simpler, you can use the await-to-js library that handles errors in the same way:
import to from 'await-to-js';
const [error, data] = await to(newUser.save());
if (error) {
// handle error
}
Creating HTTP Exceptions
When working with web apps, the common practice to handle errors is to use the HTTP status codes:
- 200 — everything is OK
- 400 — client issue
- 500 — server issue
If you’re working with Express.js or .NET Web API, you’re aware that you can send the correct HTTP response from the controllers:
// C#
[HttpGet]
public string GetData() {
return NotFound("Not found");
}
// Express.js
app.get((req, res) => {
res.status(404).send('Not found');
});
This is possible due to controller classes inheriting from the Controller
base class (in Web API) and in Express.js with the use of the response parameter (res
) in the callback function, therefore you’re able to set the correct status code response.
But what about sending HTTP errors when you’re on the service level or anywhere else besides the controller? If you’re thinking about using the default Error/Exception class, it won’t help, as the default Exception class does not have a statusCode
property.
This can be handled by creating HTTP custom exception classes manually, but I prefer to use packages that already have this logic in place.
🔴 HttpErrors (Express.js)
To create HTTP exceptions use the HttpErrors package on NPM.
# Installation
$ npm install http-errors
$ npm install @types/http-errors --save-dev (for TypeScript)
Once you’ve installed the package import it into the file where you intend to use it.
import createError from 'http-errors';
function doSomething() {
throw createError(<status-code>, <optional-message>);
}
The createError function takes two parameters, any HTTP status code, and an optional exception message.
Once you put throw in front of the createError
function, it will produce an HTTP error with the status code you specified:
throw createError(400); // Bad Request Exception
🔴 HTTPResponseException (Web API)
Use the HTTPResponseException class to throw HTTP exceptions in your APIs.
# Installation
Inside VS type throw new HTTPExceptionResponse
and the editor should prompt a package to be installed in your project.
The package lives under the System.Web.Http
namespace. Once the setup is complete you assemble the exception as follows:
throw new System.Web.Http.HttpResponseException(
new HttpResponseMessage(<HttpStatusCode>) {
ReasonPhrase = "Custom message"
}
);
The HttpResponseException
takes an instance of the HttpResponseMessage class that is instantiated with HttpStatusCode. There is also optional metadata:
- Reason Phrase
- HTTP Content
- Headers, etc.
Use the HttpResponseMessage
to form an error class,
using System.Web.Http;
var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
ReasonPhrase = "Something went wrong!",
Content = StringContent("Whatever else you want to add");
// ...
}
Then pass it to the to HTTPExceptionResponse
throw an Exception of that HTTP type:
throw new HttpResponseException(response);
🔴 Nest.js
The Nest.js framework uses the HttpErrors package under the hood and thus natively supports HTTP Exceptions.
To throw an exception simply use the exception class:
throw new BadRequestException() // Creates Bad Request Error
You can pass metadata as well:
throw new BadRequestException(
'Something bad happened',
{ cause: new Error(), description: 'Some error description' }
);
The response will be of type 400 HTTP status code containing:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
⚪ Formatting Exceptions
I like to create custom HTTP classes that encapsulate the errors. Create a class that throws a specific HTTP exception under the hood.
// JavaScript
export class BadRequestException {
constructor(message = 'Bad Request') {
throw createError(400, message);
}
}
// C#
public class BadRequestException : Exception
{
public BadRequestException(string customMessage = "Bad Request")
{
throw new HttpResponseException(
new HttpResponseMessage(HttpStatusCode.BadRequest)
{ ReasonPhrase = customMessage }
);
}
}
Repeat this for every other HTTP Exception, like Internal Server Error, Not Found, Bad Gateway, etc.
And whenever you want to use it just throw the exception class:
// JavaScript / C#
throw new BadRequestException();
throw new BadRequestException('My message');
Handling HTTP Exceptions
Now let’s learn how to catch HTTP Exceptions.
⬜ Try Catch
🔴 C#
This is how you’d handle Exceptions using Try Catch.
try
{
// Something throws an Error
}
catch (Exception ex)
{
// Handle exception
}
However, the default C# Exception class cannot handle HTTP Exceptions. To fix this, use HTTPResponseException
coming from (System.Web.Http
) namespace.
try
{
throw new BadRequestException("Something went wrong");
}
catch (HTTPResponseException ex)
{
// you can extract valuable information from ex
ex.Response.ReasonPhrase // Something went wrong
ex.Response.Headers // Response Headers
ex.Response.StatusCode // 500
// Return response
}
catch (Exception ex)
{
// Handle other types of Exceptions
}
🔴 JavaScript
The catch block has an exception object that is of type unknown.
try {
throw new BadRequestException('Something went wrong');
} catch (ex: unknown) {
console.log(ex.message); // Something went wrong
console.log(ex.statusCode); // TS will complain
// Return response
}
The HttpErrors package will extend the exception object with the property statusCode
, but the TypeScript compiler won’t recognize it, so it will complain. To address this, you can either set the ex
object to any:
catch (ex: any) { ... }
Or create a custom error class/interface with a status code and cast the ex
object into it:
interface IHTTPError extends Error {
statusCode: number;
}
...
} catch (ex: unknown) {
const error = ex as IHTTPError;
res.status(error.statusCode).send({ message: error.message });
}
⬜ Error-Handling Middleware
A common way to handle HTTP exceptions is to build a global handler in the router middleware.
🔴 Node.js
In Express.js there is a special type of middleware that is placed at the bottom of the router. Its purpose is to handle errors, hence the name error-handling middleware.
What makes this middleware distinctly different from the rest is that it has four parameters: error
, request
, response
, and next
.
app.get('/', async (req: Request, res: Response) => {
res.send('Hello Express APIvantage!');
})
// another middleware
app.use(responseInterceptor)
// controller
app.use('/api/mongoose/users', usersController)
// ----> error-handling middleware <----
app.use((error: IHTTPError, req: Request, res: Response, _next: NextFunction) => {
res.status(error.statusCode).send({ message: error.message });
});
Using the Callback-Pattern, the first parameter is an error used for capturing the current exception, while the rest are default parameters used by any middleware.
Of course, this could be applied to specific routes if you’d play with req.method
and redirect to a different source or maybe use next(error)
to forward the error to the next error-handling middleware.
🔴 Web API
C# makes use of this technique as well. However, these middlewares function like the regular ones — they’re triggered in between the request to the endpoint and the response from it. This means that this middleware can be used to log the incoming request as well as handle unwanted response.
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
// log incoming request
await _next(context);
}
catch (HTTPResponseException ex)
{
var data = new() { message: ex.response.ReasonPhrase }
await context.Response.WriteAsync(JsonSerializer.Serialize(data));
}
catch (Exception ex)
{
// Handle other types of Exceptions
}
}
}
The middleware is registered in the main file (program.cs
/ startup.cs
) and can be applied to multiple routes/controllers:
app.UseMiddleware<ErrorHandlingMiddleware>();
Or a specific one:
app.UseWhen(context => context
.Request
.Path
.StartsWithSegments("api/someRoute"),
appBuilder => {
appBuilder.UseMiddleware<ErrorHandlingMiddleware>();
}
});
⬜ Exception Filters
Much like with Try Catch, the Exception Filters are used to handle specific exception types. We can apply a filter to a:
- Specific route
- Specific controller
- Specific error
- For all errors
- For all controllers
🔴 Web API
Create a separate class and select to which filter it applies. In this case, the filter is applied to the HTTPResponseException
class we’ve used so far:
public class HttpResponseExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception is HttpResponseException)
{
// Handle HttpResponseException
}
}
}
Any time this exception is thrown, the filter will be triggered to resolve the error. Filters can be of other Exception types:
public class GeneralExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception is Exception)
{
// Handle other types of exceptions
}
}
}
And then apply globally
services.AddMvc(options =>
{
options.Filters.Add(new HttpResponseExceptionFilter());
});
Or to a controller:
[HttpResponseExceptionFilter]
[GeneralExceptionFilter]
public class YourApiController : ApiController
{
// Controller actions...
}
🔴 Nest.js
Nest.js uses Exception Filters for error handling too.
It also uses Express.js under the hood, which allows it to make use of the underlying request and response objects.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException }
from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse(); // thrown message
response
.status(statusCode)
.json({ message: exceptionResponse });
}
}
The filter can also be applied to a single controller:
@UseFilters(new HttpExceptionFilter())
export class ProductsController {}
Or globally:
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
⬜ Http Client
The HTTP Client class in C# is used to communicate with outside APIs.
If you make a call to some API:
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("http://example.com");
You can capture an exception via the response that comes as HttpResponseMessage
. This class exposes a IsSuccessStatusCode
property that can be used to easily determine if an API request has succeded or not:
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Something went wrong!");
}
else
{
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Content: {content}");
}
⬜ Http Interceptor
When working with Nest.js, you can make use of the HTTP Interceptor to handle errors. Similar to filters, Interceptors can be applied to specific routes or whole controllers.
@Get()
@UseInterceptors(ErrorHandlingInterceptor)
getProducts(@Query('name') name: string): Observable<ProductDTO[]> {
return this.productsService.getProducts(name);
}
The interceptors work on Observables. If you’re new to Reactive programming, you can start here.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
// this will intercept any error response and throw appropriate error
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
return throwError(new BadRequestException('Oh noes!'));
}),
);
}
}
CLI Exceptions
These types of exceptions are printed in the Command-Line Interface. The root cause can be a missing dependency, incorrect use of a function, unable to find the file, etc.
🔴 Common Node.js CLI Exceptions
# ENOENT (Error-No-Entity)
The error occurs when attempting to read contents/write into a file that does not exist on the device.
[Error: ENOENT: no such file or directory, open 'missing-file.txt'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'missing-file.txt'
}
# ECONNRESET
The error occurs when the TCP connection is closed unexpectedly, usually before the response is sent to the client.
Error: socket hang up
at connResetException (node:internal/errors:691:14)
at Socket.socketOnEnd (node:_http_client:466:23)
at Socket.emit (node:events:532:35)
at endReadableNT (node:internal/streams/readable:1346:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21) {
code: 'ECONNRESET'
}
# HTTP_HEADERS_SENT
Additionally, you might see this error every once in a while:
Cannot set headers after they are sent to the client
This occurs when sending the response more than once from the same endpoint or more precisely when response headers have been sent to the client, and you cannot modify them or send additional headers.
Read more on Node.js CLI Errors.
🔴 Common .NET Core CLI Exceptions
These exceptions appear when running your apps through dotnet CLI.
# MISSING_DEPENDENCY
This occurs when you’re missing a dependency in your project.
error: The package 'Package.Name' version 'x.x.x' could not be found
in 'https://api.nuget.org/v3/index.json'
Usually, dotnet CLI automatically suggests what dependency you need to install.
# MISSING_FILE
When attempting to read a file that does not exist in .NET Core CLI, the program will throw a System.IO.FileNotFoundException
exception.
Unhandled exception. System.IO.FileNotFoundException:
Could not find file '/path/to/missing-file.txt'.
at System.IO.FileStream..ctor(String path, FileMode mode,
FileAccess access, FileShare share, Int32 bufferSize, FileOptions
options, String msgPath, Boolean bFromProxy)
...
Handling Uncaught Exceptions
A graceful shutdown is when an application takes steps to ensure that all ongoing tasks, connections, and resources are properly handled before the program exits.
In terms of web development, you may encounter an exception that you haven’t handled, e.g.: unresolved promise, HTTP exception, or any other exception. If left uncaught, the exception will propagate from your app to the underlying OS and shut down your server.
To avoid these scenarios, you want to post special functions whose sole purpose is to capture all unhandled exceptions, and gracefully close all connections (terminate the process).
If you have an orchestrator running behind the scenes, it will start a new process so your app won’t stay down permanently.
🔴 Node.js Unhandled Exceptions
There are two ways to handle these types of exceptions:
process.on('uncaughtException', (error: any) => {
console.error(error);
process.exit(1); // terminate the process with POSIX signal 1
});
process.on('unhandledRejection', (error: any) => {
console.error(error);
process.exit(1);
});
The process object works like an event listener that subscribes to the event types of uncaught exception and unhandled promise rejection.
🔴 C# Unhandled Exceptions
As for C#, you can resolve unhandled exceptions using the AppDomain UnhandledException event:
class Program
{
static void Main()
{
// Will fire when unhandled Exception is thrown
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Exception ex = (Exception)e.ExceptionObject;
// Handle the uncaught exception here
};
// Your application logic
}
}
Exceptions in Event-driven Systems
As seen above the errors can also be captured using events. If you’ve ever worked with the Redis client, Sockets, Streams, or other type of Event-driven communication in Node.js, you might have noticed that they all operate on events:
redisClient.on('connect', () => {
console.log('Redis Client connected to redis...');
});
const server = require('net').createServer(socket => {
socket.on('ready', () => {
console.log('Socket server started!');
});
});
A similar can be found in C#:
using System.IO;
using System.Net.Sockets;
TcpClient client = new TcpClient();
client.Connect("localhost", 5000);
NetworkStream stream = client.GetStream();
StreamWriter writer = new StreamWriter(stream);
StreamReader reader = new StreamReader(stream);
Exception Handling using Events
We can handle the exceptions in the same fashion using the callback error-handling pattern:
// JavaScript
// #1
socket.on('error', err => {
console.error('Socket error:', err);
});
// #2
client.on('error', err => {
console.error('Client error:', err);
});
// C#
client.Error += (sender, e) =>
{
Console.WriteLine("Client socket error: " + e.Exception.Message);
};
This also works for the POSIX signal events, like SIGINT
(Signal Interrupted) event that is fired when the app process is interrupted (using CTRL+C or CTRL+D)
// Signal Interrupted
process.on('SIGINT', () => {
console.log('App is shutting down!');
});
// Signal terminated
process.on('SIGTERM', () => {
console.log('App is shutting down!');
});
Dodging a bullet
Sometimes the best way to handle errors is to avoid them. Before we wrap this up, I want to share a few tips that will help you avoid common errors.
🔴 Accessing Nested properties
Let’s say you need to read the name property of a user object and the user is possibly null. If you write it like this:
user.name;
You’ll likely encounter a Null Reference Exception (in C#) or a regular Exception that says Cannot read property name of undefined (JavaScript).
To avoid this you can first verify that the user object is not null and then try to read the name property:
if (user != null) {
user.name
}
Or make use of optional chaining:
user?.name
// it works for as many nested objects as you have
user?.name?.firstName
🔴 Extracting values out of collections
If you’re trying to access an element in a collection that may not exist, it’s always a good idea to verify its existence first.
# TryGetValue
var frameworks = new Dictionary<string, int>();
frameworks["Angular"] = 17;
frameworks["NextJS"] = 14;
bool exists = frameworks.TryGetValue("Angular", out int version);
if (exists) {
Console.WriteLine("The upcoming version of Angular is : " + version);
}
# FirstOrDefault / SingleOrDefault
In the case of a List collection, I recommend using FirstOrDefault
over First, as First will throw an Exception if no item matches the criteria.
List<Person> people = new List<Person>
{
new Person { Name = "Mirza", Age = 29 },
new Person { Name = "Ermin", Age = 28 },
new Person { Name = "Amar", Age = 30 }
};
var mirzly = people.FirstOrDefault(person => person.Name == "Mirza");
In JavaScript, retrieving an element that does not exist in the collection will just return undefined
.
const array = [1, 2, 3];
array[999] // undefined
So no need to capture an exception there.
🔴 HTTP Redirects
You can intercept invalid HTTP requests and redirect users to the correct paths to avoid getting a 404 or other HTTP exceptions.
This can be done using HTTP Client Interceptors or to keep it simple in the route handler. In Express.js, you can capture any route using the *
symbol.
// captures any path
app.get('*', (req, res) => {
res.redirect('/');
});
Once inside, you can redirect the lost user to the safe zone.
It’s a good practice to put this route at the end of the router chain so that the rule above applies to any non-existent route.
🔴 Debugging with Error-Handler
One time I had a situation at work when the app would read data from a particular table in the database and instantly crash. E.g.:
await readFromDB(); // 500 internal server error
When the error reached the global exception handler, there was no explanation about what went wrong. Just 500 internal error.
We solved it by wrapping the API call with Try & Catch, and then inside the Catch, we were able to read the actual exception message from readFromDB()
and act upon it.
try {
await readFromDB();
} catch (Exception ex) {
// ... debug ex and ex.Message
}
The issue was a mismatch in column mapping between ORM and the database. Fun times!
Keep in mind that there is no silver bullet for handling exceptions. Find the best solution that fits your project and stick with it. And be sure to put loggers in the Exceptions.
In this article, we’ve looked into a variety of ways to create and handle exceptions across frameworks and programming languages. If you want to see more articles like these be sure to hit the follow button.
Read More
Check out my other articles!