LevelUP: Promises, Promises…

Promisifying LevelDB in Node to escape callback hell…


  1. .post '/save',debug_request,who_are_you,needs_permission,(req,res)->
    	# Shorthands to enhance readability.
    	r=req.body # Request parameters/data.
    	a=req.user # Authenticated profile.
    	p=null # "Global" var to hold main modified profile while patching it.
    	# Chain everything to catch exceptions — asynchronously.
    	changes=[] # Modified profiles to save in batch, transaction like, to protect consistency.
    	new promise (resolve,reject)->resolve null # Blank promise just to bootstrap chain.
    	.then ->
    		# Validations…
    		r.name=r.name?.trim() #??? DRY
    		if not is_parent and not r.name then res.send 400,'Children must have names.';return promise.reject false # Abort chain.
    		if r.password then r.password=bcrypt.hashSync r.password
    		# Permissions?
    		if (r.username or r.current_username) is a.username and r.role isnt 'parent' then res.send 403,'Not allowed to change own role.';return promise.reject false # Abort chain.
    		if r.role is 'parent' and r.current_username isnt a.username then res.send 403,'Not allowed to make children parents.';return promise.reject false # Abort chain.
    		# If new username, check availability: prevent overwriting other profiles.
    		if is_new and r.username or is_key_change then return users.get r.username
    	.catch (err)-> # Expect notFound — means username available.
    		unless err.notFound then throw err # Only catch notFound.
    	.then (v)-> # Expected nothing; value means username exists in DB, so throw an error.
    		if v then console.log 'Exists',v;res.send 400,'''Can't change to this username — already in use.''';return promise.reject false # Abort chain.
    		# Fetch (main) profile to update. Three usecases:
    		if r.current_username is a.username then return a # Parent updating theirself; already loaded authenticated profile.
    		else if is_baby then return null # Nothing to fetch.
    		else return users.get r.current_username # Promisified LDB!
    	.then (v)-> # Expect optional value, resolved if it was a promise.
    		if v then p=v else p=r
    		# Save with new username.
    		if is_key_change then p.username=r.username
    		# Modify profile.
    		for f in ['name','lastname','password','age','gender','language'] #if r.password then p.password=r.password
    			do (f)->
    				if r[f]? then p[f]=r[f]
    		# If changing username, delete old profile.
    		if is_key_change
    			changes.push type:'del',key:r.current_username
    			# Child? Update parent, too.
    			if is_parent # Parent.
    				# Need to load all of them!
    				return (promise.denodeify mget) users,a.children
    			else # Child.
    				a.children.splice a.children.indexOf(r.current_username),1,r.username
    				changes.push type:'put',key:a.username,value:a
    	.then (vs)-> # Wait for all children to load, if promised that.
    		# Parent? Update all children.
    		if vs then for k of vs
    			do ->
    				# Fix parent reference and update child.
    				changes.push type:'put',key:k,value:vs[k]
    	.then ->
    		changes.push type:'put',key:r.username,value:p
    		# End transaction.
    		return users.batch changes # Promisified!
    	.then ->
    		res.send 200,p # Return updated profile with possibly server-side generated stuff.
    	# Error handling for everything (that wasn't already).
    	.done null,(err)->
    		if err
    			console.log '500 because',err
    			res.send 500,'Houston, we have a problem.'

Break it down!

  1. This is the key pattern requiring use of promises: the combination of branching and async:
    	if whatever then return users.get r.username
    .catch (err)-> # Expect notFound — means username available.
    	unless err.notFound then throw err
    It's ugly! But, arguably, localized: handling (synchronously) the notFound is kept relatively near the "whatever" that caused it. Technically, they still happen in separate functions — there's an implicit void return before the .catch (which is only called if some exception was thrown).
  2. ".post '/save'…->": context is an Express route handler; the usual middleware, plus custom authentication and authorization, gave us req.body, req.user, etc.
  3. "users" is a promisified LevelDB:
    db=(require 'level-promise') (require 'level-sublevel') (require 'level') 'data',keyEncoding:'utf8',valueEncoding:'json'
    users=db.sublevel 'users'
  4. There's also "mget=require 'level-mget'" which we have to promisify manually:
    return (promise.denodeify mget) users,p.children
  5. Chain abortion is a recurring pattern:
    if not is_parent and not r.name
    	res.send 400,'Children must have names.'
    	# Abort chain.
    	return promise.reject false
    Could probably DRY this with some syntactic sugar.
  6. "promise.reject false" is required to distinguish from other, unhandled exceptions.

Problems: promises suck

  1. Anything that needs that much explanation (google it) is broken: overcomplicated, for such a common usecase.
  2. I call BS on the encapsulated value excuse — it's always been a control flow issue.
  3. Localization is still somewhat violated?
  4. "You cannot really end a chain, unless you throw an exception that bubbles until its end."

Callback hell?

  1. Syntactically ugly, but not the real problem: it's the localization violation, dummies. All those trivial examples with linear flow, no branching, can still be done using anonymous callbacks — and pyramids of doom — but non-trivial flows require decoupling logic into arbitrary (named) functions, thus breaking localization, cohesion. Logic is split at async calls, not values.
  2. Callbacks suck. Sure, they enable non-blocking/async, avoiding the difficulties of pre-emptive multitasking, but suffer the deficiencies mentioned in all those promises tutorials. And they violate localization…

The real world is a special case