Skip to content

Instantly share code, notes, and snippets.

@dominictarr
Created April 16, 2012 21:42
Show Gist options
  • Save dominictarr/2401787 to your computer and use it in GitHub Desktop.
Save dominictarr/2401787 to your computer and use it in GitHub Desktop.
style guide

High level style in javascript.

Opinions are like assholes, every one has got one.

This one is mine.

Punctuation: who cares?

Punctuation is a bikeshed. Put your semicolons, whitespace, and commas where you like them.

This post is concerned with higher-order style.

Be obvious

Don't do something complex just to make your api simpler. That will lead to confusing bugs, and necessitate reading your code, while also making reading your code harder. This will be frustrating for me, when I'm trying to use your module.

Example, avoid chaining DSLs.

This is bad:

  thing.when('something').then(doThing)

It's not really obvious how when relates to dothing. It's better to pass in related things together.

  thing.when('something', doThing)
  //or maybe
  thing({when: 'something', then: doThing})

Chaining where you simply return this is acceptable.

Be idiomatic, or not.

if possible, make your code follow the APIs in node core. That way, I already know how to use your module. I don't even have to read the documentation. This is ideal.

If you don't do this, you need documentation. But even worse, is if have merely a similar API, but does not behave exactly the same, I will misuse your code, get an error and be forced to read your code. This will frustrate me.

if your can't follow an idiomatic API precisely, do something completely different.

ALWAYS PASS ERR IN CALLBACK, (an event listener is not a callback, so this doesn't apply in that case)

If you have a function called createServer, it should return a server, and it should have a listen function. createServer should never start the server listening. That should never happen until I call listen.

a listen function that takes something other than a port, a hostname, and a callback (like in socket.io) should be called something else.

my favorite API from node is Stream (more on that later)

"all you need is lambdas" -- John Lennon

Functions are your most powerful weapon. In javascript you do not need fancy plugins. Allow users to extend of control your modules behaviour by passing in a simple function, with given arguments.

connect is a good example of this.

something only needs to be faster when it's slowing you down.

don't optimise things that aren't gonna be bottlenecks, example; it's completely acceptable to read configuration files synchronously. because you only do it once, and you need to do it before anything else happens in the application.

it's something you have to read every time you respond to an http request, then it must by async.

streams in node are one of the rare occasions when doing something the fast way is actually easier. SO USE THEM. not since bash has streaming been introduced into a high level language as nicely as it is in node.

compare downloading a file one step at a time, and streaming it:

  //load the response entirely into memory
  request ('http://slow.com', function (err, doc) {
    //write the whole thing to disk
    fs.writeFile(dest, doc, function (err) {
      //done
    })
  })

now, stream it:

request('http://sweet.as') //read from the internet
  .pipe(fs.createWriteStream(pathToFile)) //write to disk as data arrives.
  .on('end', function () {
     //done
  })

the latter is preferred, the syntax is nicer, but more importantly, streams handle data in a smarter way.

they pass data along as it arrives, and can also tell the upstream to pause if they can't handle any more.

in the above example with the callbacks, the time it would take to complete is the time to request + the time to write to disk.

in the example with the streams, it will only take slightly longer than the time to request.

if the file is VERY LARGE this will be significant, also if it is VERY VERY LARGE it may not all fit in memory at once, but streaming will still work. the node.js team work very hard to make streams as fast as possible, and it's also easier, so use them.

if in doubt, modularize

node.js has the best package manager so far. This is not hyperbole. It's simple yet powerful. so split up every thing, this also makes it easier to test, and easier to extend.

I also like to separate layers,for example, if you are writing a service of some kind, write it to that you can test it entirely in memory, and then wrap it with layers that provide the network io.

conclusion

The style of a line is easy to change. But the style of a module is much more important. This is something that I think is truly worthy of debate, because it actually makes a significant difference.

what are your opinions on high-level style?

@axkibe
Copy link

axkibe commented Apr 17, 2012

Fully agree, just would add, nothing is ever that obvious that a single line of documentation would hurt.

@dominictarr
Copy link
Author

A good approach is to think 'how will I document this?' when you are writing the code.
this is a good argument for idiomatic code, because you can just reuse other documentation.
just say X return a Stream and then link to http://nodejs.org/api/stream.html

@paulkernfeld
Copy link

Hey @dominictarr, this is a nice guide. I'm curious: what's your perspective on documentation? For example, I see a lot of node projects by respected authors that don't include comments in the code. Is commenting regarded as a "band-aid," i.e. if your code is clear it shouldn't need comments?

@passcod
Copy link

passcod commented Mar 3, 2016

@paulkernfeld comments !== documentation. For example, see the regenerator runtime source by Facebook: it has very useful longform comments to explain why some things are done, or what more obscure lines actually do:

  // Try/catch helper to minimize deoptimizations. Returns a completion
  // record like context.tryEntries[i].completion. This interface could
  // have been (and was previously) designed to take a closure to be
  // invoked without arguments, but in all the cases we care about we
  // already have an existing method we want to call, so there's no need
  // to create a new function object. We can even get away with assuming
  // the method takes exactly one argument, since that happens to be true
  // in every case, so we don't have to touch the arguments object. The
  // only additional allocation required is the completion record, which
  // has a stable shape and so hopefully should be cheap to allocate.
  function tryCatch(fn, obj, arg) {
    try {
      return { type: "normal", arg: fn.call(obj, arg) };
    } catch (err) {
      return { type: "throw", arg: err };
    }
  }

And see the browserify source by @substack, which has zero comments but does have extensive documentation (complemented by a massive handbook).

@dominictarr
Copy link
Author

I normally use comments when explaining a weird thing, or maybe marking something which was a painful lesson.
Other times I write comments while I'm figuring out what code i need to write (so it's stream of conciousnessy and I might just delete it later)

Comments are not documentation (and neither are tests)!

It was a long time since I wrote this, and so somethings would change...
I guess promises are okay now. well documented and consistent enough. (not that I use them though)

If I wrote this again, I think I'd talk about secondary conotations of code. When you read code, you gain meaning not just from what the code says, but also, what it doesn't say.

for example, named functions can be called from above or below.

callAFunction(namedFunction)

function namedFunction () {...}

if that is the only time namedFunction was used, then if it an anonymous function.

callAFunction (function () {...})

then that would be immediatly obvious. This applies to scoping also. If you do use a named function once, if you define it in the narrowest scope, then it's obvious that it's not used by any other parts of the code.

On the other hand, if you define functions at the outer most scope, then it's obvious they do not depend on variables inside those scopes.
(though, it's pretty easy to see what local and non local variables they have... but that should only be an issue on long functions, which are bad for other reasons)

Also, some people like to use named functions after their code. I really hate this, although a number of coders that I respect do it. Dependencies go at the top! a function that you wrote that you are gonna call later is a function. I'd gain more information by scanning past those functions than I would from reading the name. naming is hard, so try to avoid naming if you can.

@yoshuawuyts
Copy link

Seems John Carmack thought the same:

This is going to be an unusual email – I want to talk about coding style. I’m not going to issue any mandates, but I want everyone to seriously consider some of these issues. I’m not talking about petty things like spaces around operators, bracing styles, or pointers by type or variable (although we probably should settle that one), but about larger organization of code. There aren’t any silver bullets, but months can be shaved off of a development project with a few percent improvement in productivity.

@vajahath
Copy link

Nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment