Why JavaScript on the Backend is Not That Great
JavaScript is a popular and versatile programming language that powers many web applications. It is widely used for front-end development, where it can manipulate the Document Object Model (DOM), handle user interactions, and communicate with web servers. However, in recent years, JavaScript has also expanded its reach to the back-end, where it can run on platforms like Node.js and Express.js, and interact with databases, APIs, and other services. This trend of using JavaScript for both front-end and back-end development is known as full-stack JavaScript.
While full-stack JavaScript may seem appealing for its simplicity, consistency, and convenience, it also comes with many drawbacks and challenges. In this article, I will argue that JavaScript on the backend is not that great, and that developers should consider other alternatives for their back-end needs. I will base my argument on three main points: performance, security, and maintainability.
Why do companies choose JavaScript in their servers anyway?
There are loads of examples of some popular companies switching or start off with let's say, Node.js - but ultimately having to use other solutions, further fragmenting their codebase stack. Here are some examples.
Netflix
A company that wants to build a high-performance and scalable web application that can handle millions of concurrent requests and stream large amounts of data, such as a video streaming service or a social media platform. They may choose to use JavaScript on their backend because of its popularity, simplicity, and convenience, as well as its ability to run on both the front-end and the back-end.
However, they may face challenges with JavaScript’s poor performance, single-threaded nature, and lack of standardization and optimization. They may be better served by another language that offers faster execution, multi-threading, and more mature and optimized libraries and frameworks, such as C#, Java, Kotlin or Ruby.
For example, Netflix switched from Java to Node.js on their backend in 2015, but they still faced performance and reliability issues, and they had to use other technologies, such as Java, Python, and Go, to complement their Node.js backend.
PayPal
A company that wants to build a secure and reliable web application that can handle sensitive and confidential data and transactions, such as a banking or e-commerce service. They may choose to use JavaScript on their backend because of its flexibility, expressiveness, and dynamism, as well as its ability to create interactive and responsive user interfaces. However, they may face risks with JavaScript’s lack of security and reliability, dynamic typing, and loose error handling and logging. They may be better served by another language that offers more security and reliability, static typing, and robust error handling and logging, such as C#, Java, Python, or Ruby.
For example, PayPal switched from Java to Node.js on their backend in 2013, but they still had to deal with security and reliability issues, and they had to use other technologies, such as Java, C++, and Scala, to supplement their Node.js backend.
A company that wants to build a maintainable and readable web application that can handle complex and evolving business logic and requirements, such as a CRM or ERP service. They may choose to use JavaScript on their backend because of its flexibility, expressiveness, and dynamism, as well as its ability to support different styles and paradigms of programming. However, they may face difficulties with JavaScript’s low maintainability and readability, inconsistency and incompatibility, and constant evolution and obsolescence. They may be better served by another language that offers more maintainability and readability, structure and standardization, and stability and maturity, such as C#, Java, Kotlin or Ruby.
For example, LinkedIn switched from Ruby on Rails to Node.js on their backend in 2011, but they still had to deal with maintainability and readability issues, and they had to use other technologies, such as Scala, Java, and Python, to augment their Node.js backend.
The drawbacks of JavaScript on the server
Performance
One of the main disadvantages of using JavaScript on the backend is its poor performance compared to other languages. JavaScript is an interpreted language, which means that it is executed by a JavaScript engine at runtime, rather than being compiled into machine code beforehand. This adds an extra layer of overhead and slows down the execution speed. Moreover, JavaScript is single-threaded, which means that it can only handle one task at a time, and relies on asynchronous callbacks and promises to deal with concurrent operations. This can lead to complex and messy code, as well as potential memory leaks and performance bottlenecks.
In contrast, other languages like C#, Java, Python, or Ruby are either compiled or have faster interpreters, and support multi-threading or multi-processing, which allow them to handle multiple tasks simultaneously and efficiently. These languages also have more mature and optimized libraries and frameworks for back-end development, such as ASP.NET, Spring Boot, Django, or Rails, which offer better performance and scalability than JavaScript-based solutions.
Security
Another major drawback of using JavaScript on the backend is its lack of security and reliability. JavaScript is a dynamically typed language, which means that it does not enforce strict type checking and allows variables to change their types at runtime. This can lead to unexpected errors and bugs, as well as make the code more vulnerable to malicious attacks, such as injection, cross-site scripting (XSS), or denial-of-service (DoS). Furthermore, JavaScript does not have a standard way of handling errors and exceptions, and relies on the developer to implement proper error handling and logging mechanisms. This can result in silent failures and unhandled errors, which can compromise the functionality and integrity of the back-end application.
On the other hand, other languages like C#, Java, Python, or Ruby are either statically typed or have optional type annotations, which help to catch errors and bugs at compile time or runtime, and prevent type-related vulnerabilities. These languages also have built-in or standardized ways of handling errors and exceptions, such as try-catch-finally blocks, raise-rescue blocks, or decorators, which make the code more robust and reliable. Additionally, these languages have more secure and trustworthy libraries and frameworks for back-end development, such as ASP.NET Core, Spring Security, Flask, or Sinatra, which offer features like authentication, authorization, encryption, hashing, or CSRF protection, which enhance the security and reliability of the back-end application.
Maintainability
A final disadvantage of using JavaScript on the backend is its low maintainability and readability. JavaScript is a flexible and expressive language, which means that it allows the developer to write code in different styles and paradigms, such as imperative, functional, or object-oriented. However, this also means that there is no clear or consistent way of writing JavaScript code, and that different developers may have different preferences and conventions. This can lead to inconsistent and incompatible code, as well as make the code harder to read, understand, and debug. Moreover, JavaScript is a constantly evolving language, which means that new features and standards are added frequently, such as ES6, ES7, or ES8. This can lead to compatibility and compatibility issues, as well as make the code outdated and obsolete.
In comparison, other languages like C#, Java, Python, or Ruby are more structured and standardized, which means that they have clear and consistent ways of writing code, and that most developers follow the same or similar conventions and best practices. This can lead to consistent and compatible code, as well as make the code easier to read, understand, and debug. Furthermore, these languages are more stable and mature, which means that new features and standards are added less frequently, and that the code is more likely to remain relevant and up-to-date.
JavaScript brings its own set of awful quirks
You might have seen some memes of things that could only be found in JavaScript. The language has its own set of design flaws - after all, it's a well known fact that it's only been written in 10 days.
Numbers
If you're dealing with currency or payments, stay away from JavaScript. One of the quirks of JavaScript is that it does not have a separate data type for integers and floats. All numbers in JavaScript are represented as 64-bit floating-point numbers, following the IEEE 754 standard. This means that JavaScript can handle large and small numbers, as well as fractions and decimals, but it also means that JavaScript can have precision and rounding errors, especially when dealing with arithmetic operations.
For example, JavaScript cannot accurately represent some fractions, such as 0.1 or 0.2, and instead approximates them with binary fractions, such as 0.1000000000000000055511151231257827021181583404541015625
or 0.200000000000000011102230246251565404236316680908203125
. This can lead to unexpected results, such as 0.1 + 0.2 !== 0.3, or 0.1 * 10 !== 1
. These errors can accumulate and cause significant discrepancies in calculations and comparisons.
Moreover, JavaScript can have rounding errors when converting numbers to strings, or parsing strings to numbers, especially when using the built-in methods, such as toString, toFixed, toPrecision, parseInt, or parseFloat. These methods can either truncate, round up, or round down the numbers, depending on the radix, precision, or notation, and cause inconsistencies and inaccuracies.
For example, (0.1 + 0.2).toFixed(1) === '0.3'
, but (0.1 + 0.2).toFixed(2) === '0.30'
, or parseInt('2/888') === 2
, but parseInt(2/888) === 0
.
Falsy Logic
Another quirk of JavaScript is that it has a concept of falsy values, which are values that are considered false when used in a boolean context, such as an if statement, a logical operator, or a ternary operator. JavaScript has six falsy values: false, 0, "", null, undefined, and NaN. This means that any expression or variable that evaluates to one of these values will be treated as false, and any expression or variable that evaluates to anything else will be treated as true.
This can lead to confusing and misleading logic, especially when using the equality operator, the logical operators, or the ternary operator, which can coerce the operands to boolean values, and cause unexpected outcomes. For example, 0 == false, but 1 == true, or "" == false, but "0" == true, or null == false, but undefined == false, or NaN == false, but NaN != true.
Furthermore, JavaScript has a concept of truthy values, which are values that are considered true when used in a boolean context. JavaScript has an infinite number of truthy values, which are basically any values that are not falsy. This means that any expression or variable that evaluates to one of these values will be treated as true, and any expression or variable that evaluates to a falsy value will be treated as false.
This can also lead to confusing and misleading logic, especially when using the equality operator, the logical operators, or the ternary operator, which can coerce the operands to boolean values, and cause unexpected outcomes.
For example, "hello" == true
, but "hello" != false
, or [] == true,
but [] != false
, or {} == true
, but {} != false
, or function() {} == true
, but function() {} != false
.
What about TypeScript?
Some developers may argue that TypeScript, a superset of JavaScript that adds static type checking and other features, can solve some of the problems of JavaScript on the backend, such as type-related errors, bugs, and vulnerabilities.
However, TypeScript is only compile-safe, not runtime-safe, which means that it can only catch type errors and bugs at compile time, not at runtime. TypeScript still compiles to plain JavaScript, which means that it still inherits the dynamic and weak typing of JavaScript, and that it still relies on the JavaScript engine to execute the code. Therefore, TypeScript cannot guarantee the security and reliability of the code at runtime, and it cannot prevent runtime errors and bugs, such as type coercion, null pointer exceptions, or undefined values.
Moreover, TypeScript adds another layer of complexity and overhead to the development process, as it requires the developer to write and maintain type annotations, interfaces, and declarations, as well as to use transpilers, such as Babel or TypeScript itself, to convert the code into compatible JavaScript.
Although I truly love TypeScript when dealing with frontend code, it also has its own limitations and quirks, such as the lack of support for some JavaScript features, such as decorators, or the need to use type assertions, such as as
or !
, to override the type checker. TypeScript also has its own compatibility and interoperability issues, such as the need to use type definitions, such as @types
, to use external libraries and modules, or the potential conflicts and inconsistencies between different versions and configurations of TypeScript.
The biggest issue is actually in the ecosystem
Another challenge of using JavaScript on the backend is its dependency on NPM, the default package manager for Node.js and JavaScript. NPM allows the developer to install and manage external libraries and modules, such as Express.js, or MongoDB as well as to create and publish their own packages. However, NPM is also a disaster that is insecure and prone to dependency hell, which means that it can cause security and reliability issues, as well as make the code hard to maintain and update.
For example, NPM is insecure because it does not verify or audit the packages that it hosts, and it allows anyone to publish or update any package, without any quality or security checks. This can lead to malicious or compromised packages, such as the infamous event-stream incident in 2018, where a popular package was hijacked and injected with malicious code that stole cryptocurrency wallets . NPM is also prone to dependency hell because it does not have a clear or consistent way of resolving and managing dependencies, and it allows packages to have multiple and nested dependencies, which can create conflicts and inconsistencies. This can lead to bloated and fragile code, such as the notorious node_modules folder, which can contain thousands of files and folders, and take up gigabytes of space .
In contrast, other languages like C#, Java, or Ruby have more secure and reliable package managers, such as NuGet, Maven/Gradle, or RubyGems, which verify and audit the packages that they host, and have quality and security standards and policies. These languages also have more manageable and consistent ways of resolving and managing dependencies, and have flat and explicit dependency trees, which avoid conflicts and inconsistencies. These languages also have more compact and robust code, which do not rely on external packages for basic functionality, and do not create massive and unwieldy folders and files.
Even the person who brought JavaScript outside of the web regrets some decisions around it
NodeJS is not perfect. In fact, even the creator of NodeJS, Ryan Dahl, has admitted that he regrets some of the decisions he made when designing and developing NodeJS. Some of these regrets include:
- Not sticking with promises: Promises are a way of handling asynchronous operations in JavaScript, which allow the developer to write cleaner and simpler code, and avoid the callback hell, which is a common problem in NodeJS. Promises also enable the use of async/await, which is a syntactic sugar that makes the code look more synchronous and readable. Dahl added promises to NodeJS in 2009, but removed them in 2010, because he wanted to keep NodeJS minimal and simple. However, he later realized that promises were a better way of dealing with asynchronous code, and that they could have improved the usability and stability of NodeJS.
- Not using a standard and secure package manager: NPM is the default package manager for NodeJS, which allows the developer to install and manage external libraries and modules. However, NPM is also insecure and unreliable, because it does not verify or audit the packages that it hosts, and it allows anyone to publish or update any package, without any quality or security checks. This can lead to malicious or compromised packages, such as the event-stream incident in 2018, where a popular package was hijacked and injected with malicious code that stole cryptocurrency wallets. NPM is also prone to dependency hell, because it does not have a clear or consistent way of resolving and managing dependencies, and it allows packages to have multiple and nested dependencies, which can create conflicts and inconsistencies.
- Not using a consistent and explicit module system: NodeJS uses a module system that allows the developer to organize and reuse the code, and to import and export modules using the require and module.exports functions. However, this module system is inconsistent and implicit, because it does not follow the standard and widely used ES6 modules, which use the import and export keywords, and it does not require the developer to specify the file extension or the index.js file when importing a module. This can lead to confusion and ambiguity, as well as compatibility and interoperability issues, especially when working with browser-based JavaScript, which uses ES6 modules.
These are some of the regrets that Dahl has expressed about NodeJS, and some of the reasons why he has created a new project called Deno, which is a secure and modern runtime for JavaScript and TypeScript, that aims to address some of the flaws and drawbacks of NodeJS. Deno is still in development, and it is not compatible with NodeJS, but it offers some interesting and promising features, such as:
- Security by default: Deno does not allow the script to access any system resources, such as the network, the file system, or the environment variables, unless the developer explicitly grants the permission using flags, such as --allow-net or --allow-write. This prevents the script from performing any malicious or harmful actions, and protects the system from potential attacks.
- TypeScript support: Deno supports TypeScript out of the box, without requiring any configuration or transpilation. TypeScript is a superset of JavaScript that adds static type checking and other features, which can help to catch errors and bugs at compile time, and improve the readability and maintainability of the code.
- Simpler and standard module system: Deno uses a simpler and standard module system, that follows the ES6 modules, and that requires the developer to use absolute or relative URLs, with file extensions, to import and export modules. Deno also caches the remote modules locally, and does not rely on external package managers, such as NPM, to install and manage dependencies.
Conclusion
In conclusion, I have argued that JavaScript on the backend is not that great, and that developers should consider other alternatives for their back-end needs. I have shown that JavaScript on the backend suffers from poor performance, lack of security and reliability, and low maintainability and readability, compared to other languages.
One might say that a language is just a tool. It is. It's a tool that helps make web pages interactive, as JavaScript was made explicitly for the web. It's not a shortcut for backend development, or writing horrible, heavy Electron apps.
Therefore, I suggest that developers should use JavaScript for front-end development, where it excels, and use other languages for back-end development, where they offer more advantages and benefits. This way, developers can leverage the strengths of each language, and create more performant, secure, reliable, maintainable, and readable web applications.