What the web platform can learn from Node.js: The Darwinism of small modules
developer.atlassian.com
developer.atlassian.com
If you want things to just work like in the Ruby community, make sure you shrinkwrap.
I don't think most people run into this because most backend node programmers have moved to Go at this point, so they're not aware of the upgrade problems.
On the front end, people are probably re writing the front end every 3 or 4 months. Moving from javascript framework to javascript framework. Never dealt with upgrading 6to 5 to Babel to Babel 4 to 5 to 6, and npm from 2 to 3. Or browserify, browserify react hot reload, react-proxy v1 only, gulp. Ugh.
I'm not sure it's possible to move a project using Babel 5, async/await, generators and react to upgrade to Babel 6. I've spent hours on it.
Sometimes I think it's all some kind of cruel joke. Look at Babel. I mean a library that doesn't do anything without installing a plugin. At least support transpiling ES 6 and 7 to 5 out of the box and everything else with a plugin. I have to install like 4 plugins to get anything running.
If you want to protect yourself and upgrade reliably - there are tools, like my https://github.com/bahmutov/next-update - runs your tests and only keeps upgrades that don't break stuff.
Thanks for the library.
Well, you're actually right. Babel 6 has had a lot of problems, particularly with async functions, but they are slowly being worked out. I think it's pretty clear at this point that it was released too early, and they really need smoke tests to prevent this kind of problem in future. If you've already built a significant project using Babel 5, definitely consider holding off the upgrade for another month or two.
Bug hasn't been fixed in almost a month: https://phabricator.babeljs.io/T6644
You'd think something like a basic language feature wouldn't break when you update your compiler. I did. I was wrong.
Thank the gods of software gardening that we were only using typeof in five or so places.
If you want stability, use a technology that was popular 10 years ago. If you want to work on the bleeding edge, you will be bleeding. Decide what's appropriate for your project. The reason people worked in Java in 2000 or Swift/Babel today is so they can take advantage of temporary market opportunities where other people get deterred by the pain of capitalizing on them. This can be very financially lucrative if you yourself are willing to put up with that pain.
Obviously, I'm not longer with that company, but still writing Node in the day job. As you mention, packages have minor updates with breaking changes, so if you don't shrinkwrap things explode. There is a huge number of JS packages that do a lot of interesting things but the average quality leaves a lot to be desired.
I've found people get something working very quickly with Node.js but then after a year...or three it's very hard to maintain the Node.js system. Especially when the original author is no longer on the team. This is the problem TypeScript tries to solve, and TypeScript is really nice in my opinion...but its 2015 and my computer has four cores and 4 virtual cores. Languages that do not enable developers to easily take advantage of these cores are not interesting and not something I want to invest too much time in for fear they are out of favor in the next few years when cores increase even more...
Go has this solved, but people don't quite trust Google yet. For one, they seem to have a history abandoning things (or completely rewriting from scratch them like Angular 2). Go is missing a few needed features that are preventing more wide spread adoption (things like better package management), but these features are not things that the core team is prioritizing at this time.
If your development methodology collapses when an original developer leaves, then the problem was that they were carrying tech debt by force of will rather than building a robust architecture. This isn't something TypeScript can solve.
> Languages that do not enable developers to easily take advantage of these cores...
Single-threaded runtimes, like Node, aren't intrinsically problematic, and multi-threaded solutions aren't a panacea.
If you have multiple cores and a single-threaded application that you'd like to scale, then assuming the other resources are plentiful, just spawn multiple instances and pin them to those extra cores.
> Go ...
I like Go, and its goroutines, but working with them still requires an understanding of locking and concurrency to get it right. If the plan is to throw a random front-end developer at Go and expect magic, I think that's wildly optimistic.
Not to mention, this is part of the core functionality of node. The cluster module is built in. https://nodejs.org/api/cluster.html.
Agreed on single-threaded runtimes... you can get around them, as far as robustness goes, you bring in the same techniques used to scale to multiple servers, you're just doing it a little sooner.
I haven't done enough in Go to really comment, it looks cool, though I haven't had a use case that I really needed more performance than what I already have with the other tools available.
For me it's that Go is purely an imperative language and is not ashamed of it. That's fine, it's a good imperative language, but I don't like them, so I won't use it. I don't care how great goroutines are (or are not, I really don't know), I'm not using a non-functional language again in my career.
How does Go solve this?
While Go doesn't have package management, it does have a framework (go get) for automatically fetching and compiling dependencies. However, people quickly realized that there's no way to pin dependencies; it will always fetch the master branch's HEAD, so if you "go get" a project, it will fetch some arbitary version depending what time of day it is. The community has devised various ways around it, and recent versions of Go have a way of resolving packages to a local "vendor" directory, but it's not a solved problem. There's no solution that stands out from the rest as particularly good or feature-complete when you look at the historical alternatives (Ruby's Bundler in particular).
But that may just be me. I think the same thing about generics, and others clearly don't.
FWIW, I've been using Go 1.5's vendoring alongside govendor and it seems pretty nice. While this situation is a bit of a disappointment given the generally awesome tooling in Go, it's hopefully something that will be resolved soon.
Brittle.
Before dismissing Go, please try to understand its history and the fact that it is changing.
The legacy of node will be nothing but sadness. (Sort of a joke, but also kind of true)
It seems like you have two choices if you'd like to work in the software industry: you can put up with shitty code and boring maintenance work in exchange for a steady, risk-free paycheck, or you can take on the risk that your code will fail outright and the certainty that it will be grumbled about and ultimately replaced in exchange for the intellectual thrill and potential financial rewards of working on a green-field project.
On the plus side, it keeps us employed as long as we are prepared to keep learning the new stuff, or earn a spectacular exit.
I will say for a 20 year old language, JavaScript has grown more in the past 6 years than it's first 14, and a lot of that is thanks to Node/npm.
Meanwhile in node, and I'm just picking a random path here:
webpack -> watchpack -> chokidar -> anymatch -> micromatch -> extglob -> ansi-green -> ansi-wrap
(I might just write a tool to find the largest such path in all of npm.. accepting bets)
I too am confused about Babel 6... but to be fair I haven't looked too much into it. I just think it's weird that something as "simple" as doing ES2015 -> ES5 now has plugins and a whole ecosystem around it. I understand the creator's ambitions, but I'd prefer to just install it and plug it to my build system.
I don't know much about Go, but I am personally looking into Elixir as another backend choice.
That's why the change, I do think they pulled the gun a bit soon though, but many would have jumped to it just the same.
I loved node when it came out. I did a lot of experimental projects with it. I also liked npm and the ability to use modules...
But at some point, this all turned into dependency insanity.
It didn't bite me as much, because i've never used node for production apps, but I imagine it has the potential to be a serious issue, when your bread-and-butter app depends on 694 other libraries.. who knows what lurks in them...
What did bite me was the apparent "infinity" of possibilities at each step of the development process. Whenever I need a feature - there's 20 different libs there to help you. Which one should I pick ?
The problem is this approach that somehow finds a monolith with 694 dependencies acceptable, yet turns around and praises node.js modules for being so "bite-sized".
>What did bite me was the apparent "infinity" of possibilities at each step of the development process. Whenever I need a feature - there's 20 different libs there to help you. Which one should I pick ?
This kind of choice paralysis used to bite me too. What totally fixed it for me was really weighing the consequences of adding a dependency every time I'd consider one. Most of my production services are a couple hundred lines and have fewer than 5 dependencies (of which, half are boilerplate that every other service has).
Rails is great at big CRUD MVC applications. If you want a Rails-like app in Node.js, you should start with an opinion "skeleton" MEAN/MVC framework rather than re-implementing rails and all its dependencies with every application.
Comments like this are what make it difficult for me to take HN seriously anymore. It's just an example of the hivemind at work, assume that an entire industry has the same ideas as you about what technology to use.
The Node/JS ecosystems are experiencing a Cambrian explosion in technology, with all that implies. If you don't like it, don't worry. It probably won't take 20 million years this time to settle in on the "best" ways of doing things.
For some of these core tools, authors explicitly state that windows is a second class citizen. It's just not worth the expense to support something that has so few comparative users, in some cases (ruby). It's a lot of work for something that will never feel "native".
So I installed Ubuntu Server (latest) in a Virtual Machine via Hyper-V... set it to auto-boot.. setup OpenSSH, setup Samba ... I kind of just muddled through it, to be honest.. after that, I setup Docker, build-essentials and nvm, which covered my needs there.
On the windows side, I use Clover as an explorer replacement (file explorer, not the desktop), and use ConEmu for tabbed shell interfaces. It works pretty well for me. I use GUI stuff in windows, and the linux side via SSH.
I don't think node on Windows is so bad these days once you've got Python 2.x and VS 2015 Community installed... When I started with node (0.6-0.8 era), it was pretty bad in Windows. These days most popular modules just work, or work once you have gyp prereq's installed.
We keep our dependencies up-to-date by occasionally running "npm outdated", checking the packages for their Changelog.md (or equivalent) and then enjoying the new stuff.
From my perspective you were just unlucky with your packages, or you might be doing something else wrong.
Most of the developers where I am are running OS X, which is significantly more similar to our Linux production environments than is Windows and yet we determined that it was just easier to always run our node applications on Linux guest VMs. On Windows, I wouldn't even have the gall to try to make it work without this kind of setup.
"Run anywhere" is really only descriptive of languages (e.g. Java), not implementations (e.g. Dalvik). You can't have a "run anywhere" implementation, unless you are grouping a bunch of implementations together...one for ARM, one for x86, one for FreeBSD, one for Windows, one for Minecraft, etc... because even under the same name the implementations would very significantly internally.
"Run anywhere" is a function of (1) the language spec (i.e. how much machine dependent behavior is permitted) and (2) the actual real-world support across platforms.
JS has the first, but the second is often conceeded, per the earlier comment.
You need to install the proper prereqs for node-gyp, but those are clearly explained on the node-gyp docs.
It still didn't work until I stumbled upon the --msvs_version flag for npm i. And this is for a dependency in mongodb!
Python 2.x is the bigger oddity to me... seems like python's role with gyp should be replaced with something self-bootstrapped at this point.
I spend about equal parts of my development time between OSX, Linux and Windows... they all have had pain points for me. The only difference is in OSX dev you're likely to have XCode & homebrew. In windows, you may have VS, and probably won't have Python ^2.7.0 installed.
Part of why I've at least participated enough to raise a possible bit of help with the situation[1]. There are times when all you can reasonably do is raise the issue, and suggested fix... there are others where you should proactively fix it.
Node/npm's community is very open-source driven, and very diverse. I don't liken it as much worse than any other platform, and far better than most.
1. Shrink-wrapping: yes, of course. Semver ranges never made sense to me. All changes are breaking changes. The idea that we could safely auto-upgrade a project is insane. A semver range is an open invitation to allow people not on your team to arbitrarily change your code at any time in the future. If it was up to me, npm install -save would just shrink-wrap. The idea behind semver ranges is fundamentally flawed since it relies on a human making a determination of whether the change is patch or major or what have you. Even if you automate it and tie it to tests you can't know for sure: Even in a magical world where a "patch" upgrade could be guaranteed to not actually break behavior, it could still have dire perf consequences (which in an async world could absolutely change the behavior of your running program by surfacing new race conditions or what have you). That's why this is the behavior on tonicdev.com: when you require a package we magically shrink-wrap it at that point in time. Reproducibility should be the default. All changes are breaking changes.
2. The quickness of change is definitely also an issue. This may just be a price of progress thing though. I certainly wouldn't want to give up amazing rewrite the world changes like the development of React for a "stable" environment. I can also see the flip side to this though.
We have apps that are stable and have been running for many months, until suddenly we need to add a small feature. With a missing package deep in the tree — not one we were depending on directly — we have to waste several hours being forced to upgrade packages to newer versions, just to work around the missing version. It's insane.
(It's possible to unpublish NPM packages, which is what happened here. That in itself is terrible, especially since all you get is a 404 from NPM — no reason for the unpublish provided.)
The NPM tool itself is a mess:
* It suffers from sporadic bugs (the dreaded "npm ERR cb() never called") and isn't resilient against network issues (e.g., timeouts).
* It can't use its own cache for packages that have compiled artifacts.
* npm-shrinkwrap.json isn't stable. Adding a package tends to rewrite the whole file depending on whether it has resolved packages from the network or locally.
* Lots of surprising disconnect between shrinkwrap and node_modules. For example, "npm i package" does nothing if it's in the shrinkwrap file, but "npm i package@latest" upgrades (but doesn't, obviously, modify the shrinkwrap file).
* It uses a lot of files. We've had inode issues on some boxes because the sheer size of the NPM cache + deploys use so many files. Production deploys includes all sorts of artifacts (tests, documentation, readme, etc.) that you can't exclude.
* The nested tree (fixed, I hear, in NPM 3.0) is really unmanagable.
The microscopic, mutating nature of NPM packages is chaotic. There's no practical way to do upgrades in a controlled manner. (My wish list item: A command that prints out all upgradeable packages and a complete changelog for each version bump.)
NPM could learn a lot from Ruby's Bundler.
Has it ever been better though? Technically, you could argue that webdev has never been as good as it is now.
Not that I like the current situation, I just think that things in webdevland are better now than ever before.
I can't talk about Node on the server since I don't use that - already doing way too much javascript for the frontends - but the frontend side is hard to keep up with as well.
Between all the Babel and Webpack plugins it's hard just to get a working setup and the `do one thing and do it well` should not be a bigger thing than user experience.
TypeScript makes writing JavaScript less painful but doesn't solve the ecosystem for me: I do rm -rf node_modules/ && npm install semi regularly to make sure everything works (not using shrinkwrap yet) on a fresh install and I get install errors way too often (peer dependencies etc).
To save you time, don't bother with npm3 for now as it is extremely slow.
Node as npm, Perl has CPAN. Why are modules hard in Node, when modules in Perl work?
That seems crazy! Maybe I am out of touch but how is that even viable or smart? Seems like a time & money suck both of which can be spent on so many other valuable areas. Does the consumer or whoever the site services really care about what front-end framework is being used? No, they just want a decent user experience so why not focus on that rather than the latest shiny framework. I just don't get it.
I feel like some devs have lost site on what's really important with developing software. It is not about the latest shiny thing. It is about the target customer.
This is the case in any environment. This is the case for your OS, for python requirements, for java jars, everything. Your versions should be part of the source control (not necessarily the dependencies in your source, but at least a project file that has the versions listed).
Python has a requirements.txt. It's best practice to pin your versions in it.
I've been trying out jspm recently. It's quite nice and teaches you this out of the box. You pull down a npm package, it will put the version into your package.json so everyone can use the same version.
Yes the versioning is sad and breaks almost every time. Also some modules look fragile and what was good yesterday is not good today anymore.
But still. Those are the reasons why I still stick with NodeJS :
1. Electron. Simply awesome - I copied 90 % of my app, changed Sequelize ORM to have it's storage to sqlite and boom. I have a webapp as a OS-packaged app.
2. Isomorphic code - I'm rendering React / Flux applications on the server-side and on the client-side. Reusing code, storage and syncing logic between them. Even my logger module works on client-side / server-side.
3. Front-end / back-end needs very similar skills - This is one of my top reasons to tell my clients ( I work as a freelancer ). There will not be a big deal for a front-end guy to write / understand backend code and vice-versa. Also it's javascript. The most popular language.
4. Libraries for most popular services - There is always something, someone wrote that is NodeJS or at least javascript.
P.S. I'm heavily using TypeScript nowadays. I think this solved a lot of NodeJS syntax/compiler problems.
And no. Things break all the time and still give value to customers. Things break for A reason and still give value to customers.
Yep it would be cool and more non-breaking if 20-years of Java experience was injected into NodeJS, but I bet in 20-years NodeJS will be even better than the popular alternatives these days.
Sometimes the value to customers is just a system that works without downtime or failures, and the next increase in value is not even related to something that was built on this bleeding-edge packaging model. The only types of development projects that seem to benefit from it are the kind of project that can dedicate a full-time or mostly-full-time developer to it forever (forever being the lifetime of the product/service).
If dependency issues crop up every few months for a perodically updated project that could almost be a death knell, if you have to pivot or do major work on the backend all of a sudden you could come back to just a broken set of shambles. So the only thing it fits is a really strange gap between hobbyist/side project and stable production which ends up being just plain risky for a technology choice.
IMHO Node isn't built on this mindset ( endless churn ). I think it's just rapidly evolving. It's just enthusiasm by the community.
As far as my experience goes, I just put more effort in finding the right module rather than finding >any< module that will do the work. Afterwards I can usually npm-shrinkwrap, upload to DigitalOcean and leave forever.
And your users get to enjoy awful performance and insane resource usage for their app. How generous of you.
Also, the hate for NodeJS is because Javascript is a terrible language, no matter how many things you put on it (TypeScript, Babel, etc.). It's also because V8 performance is laughable.
Everything you mentioned is also mentioned in http://notes.ericjiang.com/posts/751 , which I believe does a nice job of explaining why NodeJS is terrible.
About Electron. I think nobody will build Photoshop or 3D Games ( at this point ) using it as a platform. I would personally decline if I have an assignment like this. But for apps like Slack, Visual Studio Code, Atom, etc, it works pretty good. Something more. I'm actually working on my own accounting app that suits my needs, which I would not be able to do for that limited spare-time I have these days If I used Objective-C / Swift.
I won't get into a long rant, but aside from on OSX, Slack and Atom are slow, resource hogging and unstable piles of crap. Atom couples that with bad architecture and engineering choice, but that's not Electron's fault.
(Also, your comparison with English is bad. English naturally evolved to the state it is in today. Javascript was designed from scratch, and it still manages to be awful. Also, sorry, Javascript isn't the most common language, thankfully.)
How? (Other than choosing HTML and JS in the first place)
Ah, so they're better on OSX? On Windows 7 all the slack.exe processes are currently using 780MB RAM in total, and I'm only signed into 3 Slack communities.
Shocking!
That's the key right there "Well enough"...
Atom is my primary editor and I am extremely satisfied with it. Your self-righteous arrogant attitude is strange and has nothing to do with objective, rational criticism.
Compared to what? Objective-C? Dalvik? (JavaScript VMs generally achieve better performance than those for systematic reasons.)
But don't take it personally. HN truly has hate for every language (name a language it doesn't hate!), and the internet supports sharing your hatred with others more than it supports sharing love.
You've found a system that earns you money by writing software, and that itself earns you some respect here.
First of all, most people don't actually follow semantic versioning, but npm's default version ranges assume they do, so unless you lock down your dependencies with `npm shrinkwrap` you'll find your app occasionally breaks because some deeply nested dependency broke something in some package you don't know anything about.
But shrinkwrap has a bunch of problems itself (so many that Uber wrote a separate tool to try to fix it: https://github.com/uber/npm-shrinkwrap). npm-shrinkwrap.json files are huge, in part due to the nature of npm, but it also contains a bunch of unnecessary information. The bigger problem is it's non-deterministic, resulting in huge diffs every time you run it, even if nothing has changed.
Here's another fun bug I encountered recently: https://github.com/npm/npm/issues/9642
Every time I think I've worked around some bug another pops up. I wish I've kept track of them all.
/rant
I agree that other ecosystems could learn a lot from the Node philosophy. And in general I enjoy working with the technology. But it's not all roses. In an ecosystem of small modules, you often find yourself wading through many -- equally small -- annoyances which can add up to a lot of friction. Here's a contrived fake example:
Your app needs ABCXYZ. So you go shopping for a module. The most popular one looks good, but when you Browserify it your bundle ends up with 1,000s of extra lines of code for the Buffer shim. So much for keeping that footprint small. So you keep looking and find another one that has a good footprint, but a weird API and it hasn't been updated by the maintainer since 2011. The next one you find looks great, but the maintainer decided they were tired of ES5 and didn't feel like adding a distfile, so you would have to install a bloated transpile toolchain to use it. You're getting annoyed but you keep going. The next one's author inexplicably decided to implement everything with streams. But you're too busy to spend a day grokking streams and reworking your entire API to be compatible. The next one is packaged as a jQuery plugin. The next one doesn't have a license. The next one uses Web Workers for no reason you can see. Etc. etc. etc. Repeat.
Any of these could be problems with a monolith (and monoliths certainly come with their own pile of frustrations), but monoliths are less likely to hit these problems simply because they consolidate effort and personal emotional investment around a certain set of techniques and standards, which leads to more consistency overall.
$ npm install xhr
$ browserify -r xhr | wc -c
11147
$ browserify -r xhr | wc -l
390
$ browserify -r xhr | grep Buffer
$There are several times I've found something close to what I want, I fork it on GH, change the bits I want, update the name everywhere and publish it... many times they continue to work as-is without much grief. Of course that's part of why there are so many options for any problem.
I usually look at the following, when was the last commit, and how many issues have been sitting stale for over a month... if the last commit was several months ago, but there aren't any recent issues, I'll check through the backlog issues.... some libraries reach maturity and don't change much, if the issues aren't recently curated, I move on...
thrift is the only module where I've locked the version number in package.json.
Maybe it's easier because I don't shrinkwrap, I also get the latest version when CI runs and that allows me to make sure that my application is always compatible with the up-to-date releases of all the dependencies. Shrink wrapping has the downside of actually making it harder to upgrade dependencies because you can get further and further behind on updates.
Fundamental core patterns like streams are still broken and suffer from huge UX issues. I hope future versions will use promises and async/await in core and redo streams to utilize this more like Go's reader/writers. Just by punting that stuff to core the UX without third-party modules would improve drastically, and encourage better practices throughout third-party code as well.
I'm not bashing Node I'd love for it to do great. I'd love to use Lambda out of the box with no callbacks and not worry about which streams are missing which handlers or does this third-party module implement them correctly or will it explode on me :D.
I've had to back port security patches far more than I would ever like to admit.
The node community is still pretty immature. Sure it's nice to rush ahead releasing version after version without having a LTS version but when I want to build something serious with it it can be downright frustrating.
In my experience, it's a bit of a shit show. You end up with libraries that are subtly incompatible, or have overlapping functionality, or strangely large holes of functionality that require overlapping combos of libraries and therefore nobody's written.
If you get your socks off writing shims and glue and maintaining it against a thousand moving targets, power to you.
In my opinion, what you really want is someone to bundle composable libraries for you. That bundling has incredible value. You know that everything more or less works, in ways that you'd expect, because someone else managed to test it.
You have all the functionality basics down pat. The shims you do have to write work at higher levels of abstraction. Etc, etc.
Sooooo a big monolithic framework like Rails or Django? I thought we said those are bad.
Edit: I know this doesn't solve the problem completely, but comparing, for instance, a typical CPAN module's versioning scheme with what you get in npm, I'm far more confident in a lack of breakage in the npm case. :)
Edit2: For downvoters: I program in Perl and Javascript professionally. I'm speaking from experience (my boss just ran into this exact problem with a cpan module in production yesterday), not picking on Perl.
This can be painful when you're dealing with a really big project and someone provides a security update but it's only for the newer version that has lots of breaking changes.
I'm curious to know how other dynamic languages handle this. In Perl I think you generally can't safely load >1 version of the same module, so while you could end up with breakage on an update, you can't end up with the above security problem.
The idea that you should use a module to check if a number is -0 is just insane.
Developers need to wisen up and accept the reality that there is no right or wrong way to do anything. The problem is that people like radical thoughts. No one wants to hear the word 'sometimes' - Which is actually the right word to use 99% of the time.
Question:
- Should you use as many small modules as possible?
Answers:
- Famous developer: Yes, always break everything into small modules because it encourages code reuse, composability, collaboration. That's the Unix philosophy.
- Moderate developer: Small modules are great sometimes but in some cases you just want a wholesome tool that just works and takes care of all the edge cases (and gives you operational guarantees). Having too many separate modules increases management complexity and can make the upgrade/deployment process more complicated and time consuming. Sometimes having too many small modules can make your code difficult to read because people are forced to read through pages of documentation on GitHub every time they encounter a new module.
Question:
- Is Microservices always the right approach to software development?
Answers:
- Famous developer: Yes, always break up your system into separate services - This promotes separation of concerns which makes your system easier to manage.
- Moderate developer: Sometimes. If you have a small team, you don't want to be managing too many different services and having too many code repos - Sometimes it's better to start out with one service, but build it in a way that it can easily be split out later on.
Question:
- Should you use Docker?
Answers:
- Famous developer: Yes, you need to Dockerize your whole stack from the beginning if you want fast deployment.
- Moderate developer: Sometimes. You should be selective about which technologies you use within your project and try to keep the deployment process as simple as possible. If you can keep your technology count to a manageable level, you may end up not needing Docker.
Question:
- Should you use promises in JavaScript?
Answers:
- Famous developer #1: No, promises are evil, they are slow, they swallow your errors and they make your logic impossible to follow.
- Famous developer #2: Yes, promises should always be used. Callbacks make code difficult to read and cause callback hell.
- Moderate developer: Promises are good sometimes but you have to be careful how/when you use them. You don't want to have really long chains of promises spread across multiple files. Sometimes callbacks are more appropriate if you design your system in such a way that nesting is minimal. Ultimately is comes down to what makes your team happy.