Fact: Node.js is single threaded yet powers some of the highest traffic sites (source)
Node.js is a unique gem. When harnessed for the right problem and in the hands of a knowledgeable developer it would shine for you, but use it for unsuitable tasks or by a developer unfamiliar with Node’s philosophy then chances are you’re headed for a fall.
Recently we delivered a couple Node.js servers for a client who never imagined Node could be a viable framework for his business. Concurrently while consulting for another client we found they totally misused Node.js and were better off writing their servers on any other framework than Node.
This, along with many other misconceptions we came across along time, made me realize there is a lot of confusion surrounding Node.js, and it would be in order to write a short guide to help managers better understand this framework, when to use it and what to watch out from.
Fact: 0.14% of the servers worldwide are Node.js based (source)
But First – What Is Node.js?
Break It Down For Us!
- Node.js is not a server (although it can, and many times does, run server code)
- Run automation tasks (e.g. Grunt tasks)
- Open a listening socket and act as a simple web server
- Run as a full fledged web application server (when combined with the Express framework for example)
- Run worker/scheduled/background tasks (e.g. consume queue messages, perform backups, send invoices etc.)
- Act as a push server to mobile or web apps using Socket.io and/or APNS/GCM push server integration
- When it has no more pending callbacks or code to execute it simply exits
Single Threaded (*)
- It can only do one thing at any given time
- When running as a server, a single client’s unhanded exception could take down the entire server
- CPU intensive operations would choke your application and adversely affect all connected clients. This means for example that performing Fast Fourier Transforms or parsing XML trees are definitely not good use cases for Node.
- There are no race conditions, no concurrency problems, no need to lock shared resources, use mutexes, monitors or semaphores
- It only utilizes a single GPU core
* Truth be told this is a bit of a lie. Node.js actually does utilize a pool of kernel worker threads (via the libuv library) as part of its Event Loop mechanism, for managing asynchronous I/O bound operations behind the scenes and conveying the results (or failures) back to our application.
- Node.js is asynchronous by nature and raises events to invoke callbacks when an I/O operation is complete, when there is an error or upon other important events pertaining to the operation
- Writing event driven code is different from writing procedural code, and requires different practices and tools (e.g. promises, async et.al)
- Developers unfamiliar with event driven frameworks may face a bit of a learning curve, and need to adjust their coding practices accordingly
- Traditional exception handling (Try/catch blocks) are pretty much useless in many cases, because exceptions thrown in the event loop context are asynchronous and try/catch blocks are synchronous by nature.
Non Blocking I/O (Input/Output)
- Node has an Event Loop for handling I/O bound operations. Whenever there is a read/write operation (socket, stream, file, cache, datastore , …) Node pushes it onto the event loop queue. When the I/O operation is complete (i.e. our data is ready), our application’s callback is invoked
- This means that Node’s single application thread never waits/blocks on I/O
- It also means Node is highly efficient, and can (theoretically) handle up to 1 million concurrent connections, as opposed to Apache/IIS for example that can only handle few thousands
So What Is The Case For Node?
Here are some examples of where Node.js could be a perfect fit:
- Online games, Chat, Data streaming
- Stock trading dashboards
- Leader boards
- Messaging applications and message pushing
- AJAX heavy mobile/desktop SPAs (single page applications)
- Where we need a persistent server-browser connection
- When we want code reuse across client/server
What is NOT A Case For Node?
- Heavy computation/processing – CPU intensive work annuls the throughput benefits of Node (*)
- Simple CRUD HTML apps – there are much better alternatives such as Django, Laravel, RoR, Zend Framework, etc.
- Web applications with many routes, controllers and views – similar to previous bullet
* Heavy computation should be offloaded to background processes, for example by using a queues / producer-consumer distributed architecture.
Here are some points to watch out for when designing a Node.js application:
Error and exception handling
Node.js and it many modules do not enforce a single unified way for conveying errors. We have modules emitting error events, functions returning errors, special error callbacks, error parameters, throwing of Error objects or even worse other objects (e.g. strings) as exceptions.
Not being able to understand and properly handle errors and exceptions with Node.js means your application/server is running at very high risk. Since Node runs a single thread even a minor unhandled exception could crash and burn your server.
Modularization & Design Patterns
Node.js is all about modularization. Whether it is its built-in modules, external modules or proprietary/private modules.
Developers need to understand the concept of modularization and follow it when writing proprietary code: divide-and-conquer small files, separation of concerns, exposing a public interface while hiding private members and implementation details, and using relevant design patterns, IIFEs, event emitters and prototypical inheritance.
Writing event oriented code is very different from traditional procedural programming, so developers meaning to write Node.js apps must come prepared or make the leap.
It is very common for find Node newbies running on wild goose chases, trying to understand why a variable is null immediately after reading data from Redis for example.
Scopes, closures, IIFEs and hoisting are just a few subjects that must be mastered before trying to mount the Node.js bull.