161 lines
7.8 KiB
Markdown
161 lines
7.8 KiB
Markdown
---
|
|
layout: blog
|
|
title: Guide on error handling
|
|
published: true
|
|
date: 2020-12-09T14:44:11.948Z
|
|
tags:
|
|
- Development
|
|
- Guide
|
|
---
|
|
Having good _error handling_ practice is very important when building complex applications.
|
|
Applications which consist of multiple applications such as web applications,
|
|
where you have multiple parts like _client_, _server_ and _database_ create a system where
|
|
errors can happen in any individual parts or in communication between them.
|
|
|
|
Let me show you some examples of what kind of errors there might happen and how to treat them.
|
|
|
|
## Common scenarios
|
|
|
|
Don't pay attention to the coding style here.
|
|
There might be a different kind of libraries and conventions used in applications.
|
|
Code samples are written just for the demonstration of the various scenarios and what should happen in those scenarios.
|
|
Please read through the comments as they explain how errors should be treated.
|
|
|
|
### Back-end application - Single run script or a CRON job
|
|
|
|
```javascript
|
|
try {
|
|
// This can be any type of action that might fail
|
|
Database.connect().insert({ value: 'Some information' })
|
|
} catch (err) {
|
|
// Is important to **log these** errors with highest priority (error)
|
|
logger.error('Error happened while trying to insert some information', err) // Error context is passed here
|
|
|
|
// Should the script continue?
|
|
return
|
|
}
|
|
```
|
|
|
|
It is necessary to pass the error context to the logger so the error can be traced later.
|
|
|
|
### Back-end application - Responding to client requests
|
|
|
|
This is usually how web applications are built and the most common scenario where errors should be properly handled as it might be a security threat.
|
|
|
|
```javascript
|
|
app.post('/message', (req, res) => {
|
|
const { message, author } = req
|
|
try {
|
|
Database.connect().insert({ message, authorId: author.id })
|
|
} catch (err) {
|
|
if (err instanceof ValidationError) {
|
|
// Validation errors are expected to happen often.
|
|
// They are caused by a bad input from the user.
|
|
// There is no need to log these errors as they don't cause a system crash.
|
|
|
|
// Client should be informed on the matter of what went wrong with the request.
|
|
res.status(400).json({
|
|
message: `Invalid input. ${err.message}`
|
|
})
|
|
else if (err instanceof RecoverableError) {
|
|
// As a `RecovarableError` we can think of an error that most usually doesn't happen
|
|
// but can and we have a way to continue the rest of the process and handle the error recovery later.
|
|
// As an example we can imagine a case of sending an email to users with some information.
|
|
logger.error('Error while sending mail', err)
|
|
|
|
// Sending an email is not a crucial part of the functionality and can be done later.
|
|
// So we continue the functionality but we log the error so we know that we have to handle it.
|
|
res.status(200).json(data)
|
|
} else {
|
|
// This is an example of an **unexpected** error.
|
|
// It doesn't only apply to a `Database`. You can replace the `Database` with any other 3rd party system.
|
|
// These type of errors can be programmers fault for not handling certain scenarios correctly,
|
|
// but also be caused by unexpected events happening in the system
|
|
// (ex. Database server is down, usage of the wrong ID of relations in SQL queries)
|
|
|
|
// In this case we want to `log` this error with full context,
|
|
// so it can be found and fixed if it is a programmer's fault.
|
|
logger.error('Unexpected error happened', err)
|
|
|
|
// Client should be informed of such an error but without the context, as it might reveal proprietary information about the system
|
|
res.status(500).json({
|
|
message: 'Unexpected error happened'
|
|
})
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
It is very important to **keep all proprietary information** about the system **secure and private** only to developers and system administrators.
|
|
It is a **security threat** and it might be abused by malicious hackers when any of the information gets leaked.
|
|
It doesn't matter if the information is shown to the users or not, while it's being sent to the client it can be discovered. I
|
|
|
|
However, if there is an additional error handling functionality in place which
|
|
is able to filter out the error context in the `production` environment,
|
|
it might be a good practice to include the error context in the `development` environment,
|
|
as it will speed up the error discoverability.
|
|
|
|
To implement such a feature I'd recommend splitting `message` types sent to the client,
|
|
so there isn't a chance that someone would accidentally send proprietary information to the client.
|
|
|
|
```javascript
|
|
function sendError(body) {
|
|
if (process.env === 'production') {
|
|
return omit(body, 'errorContext')
|
|
}
|
|
return body
|
|
}
|
|
|
|
res.status(500).json(sendError({
|
|
message: 'Unexpected error happened',
|
|
errorContext: {req, message: `Error while submitting ${req.path}`, err}
|
|
})
|
|
```
|
|
|
|
### Front-end application - Handle an error from back-end
|
|
|
|
When an error happens on the back-end we should show this error to the client so the user knows that his action was not successfully fulfilled.
|
|
|
|
If the back-end handles errors correctly and sends the client an appropriate message, the client should be able to just show the informative description of the error that occurred.
|
|
|
|
#### Translating errors
|
|
|
|
If your application is built with any **internationalization** framework or with a translation system in general,
|
|
there has to be some kind of mapping from error sent from the back-end to the translated message.
|
|
|
|
Simple mapping could be done by having a table of error codes for any application error that might happen.
|
|
These codes don't have to be strictly numeric. They can consist of some rules like having separators to distinguish
|
|
services or parts that fail and which way. Examples `RESOURCE-404`, `POST-404`.
|
|
It's a good practice to have always a default message for unexpected errors which often do happen.
|
|
|
|
### Front-end application - Network Error
|
|
|
|
These types of errors are least expected by developers but they happen regularly.
|
|
Users usually lose connection when they browse while traveling or are connected to an unreliable mobile connection.
|
|
It should always be anticipated that these errors can happen and the application should recover
|
|
from the failure and allowed to retry their action.
|
|
|
|
### Front-end application - unexpected error in application code
|
|
|
|
Errors which happen only on front-end are often a coding error.
|
|
Most common examples would be not covering all possible cases of application logic or accessing properties
|
|
that are undefined because of the unexpected shape of response data.
|
|
While back-end applications can recover from their errors by restarting the process,
|
|
it doesn't apply to client applications where these types of **errors might cause a crash** or a **freeze** of an application.
|
|
Some frameworks allow to **recover from these crashes** by implementing an **error boundaries**.
|
|
See [_React_ Error Boundaries for example](https://reactjs.org/docs/error-boundaries.html).
|
|
I can only highly recommend using such a type of recovery.
|
|
|
|
If you use an external error monitoring tool like [Sentry](https://sentry.io/welcome/) or [bugsnag](https://www.bugsnag.com/), don't forget to log these errors from the error boundary.
|
|
|
|
## Rules
|
|
|
|
To summarize this we can create a list of rules:
|
|
|
|
- Error messages have to be informative for the client, but they **can't reveal private and proprietary information** about the system architecture
|
|
- Errors should be logged only when they require additional action
|
|
- Errors that are recoverable should not crash the system
|
|
- Unexpected errors should turn into expected errors and be handled properly
|
|
- Failures between connections of the systems should be anticipated
|
|
- Client applications should be always able to **recover from unexpected errors** just as from expected ones
|