Handling errors where they occur - midstream - obscures, in use case terminology, the happy path/basic flow/main flow/main success scenario. That doesn't mean that other flows/scenarios aren't important - on the contrary. That's why there are extensions/alternate flows/recovery flows/exception flows/option flows.
Clearly identifying the main flow in code is valuable.
Whether or not an IDE can collapse all the conditional error handling is beside the point - especially given that traditionally Java has been chastised for needing an IDE to compensate for all its warts.
to implement functional try/catch
Granted Go doesn't use a containing tuple like [error, result] but destructuring still directly exposes null or undefined values which is rather "un-functional" - typically the container (the tuple) is left as an opaque type while helper functions are used to work with the contained type indirectly (e.g. Rust's Result).
Now your criticism regarding try … catch is fair …
a variable ripe for mutation
… but JavaScript isn't a functional language (which typically is immutable by default and supports the persistent data structures to make that sustainable). However it is possible to take inspiration from Rust:
If a variable has unique access to a value, then it is safe to mutate it.
e.g. for local variables with exclusive/unique access (i.e. aren't somehow shared via a closure) mutation can be acceptable.
So maintaining a context object to "namespace" all the values that need to stretch across the various try … catch scopes isn't too unreasonable:
consthasValue=(result)=>result.hasOwnProperty('value');classResult{staticok(value){returnnewResult(value);}staticerr(error){returnnewResult(undefined,error);}constructor(value,error){if(typeofvalue!=='undefined')this.value=value;elsethis.error=error;}getisOk(){returnhasValue(this);}getisErr(){return!hasValue(this);}map(fn){returnhasValue(this)?Result.ok(fn(this.value)):Result.err(this.error);}mapOr(defaultValue,mapOk){returnhasValue(this)?mapOk(this.value):defaultValue;}mapOrElse(mapErr,mapOk){returnhasValue(this)?mapOk(this.value):mapErr(this.error);}andThen(fn){returnhasValue(this)?fn(this.value):Result.err(this.error);}// etc}constRESULT_ERROR_UNINIT=Result.err(newError('Uninitialized Result'));// To get around statement oriented// nature of try … catch// wrap it in a function//functiondoSomething(fail){// While not strictly necessary// use `context` to "namespace"// all cross scope references// and initialize them to// sensible defaults.//constcontext={success:false,message:'',result:RESULT_ERROR_UNINIT,};try{if(fail)thrownewError('Boom');context.success=true;context.result=Result.ok(context.success);}catch(err){context.success=false;context.message=err.message;context.result=Result.err(err);}finally{console.log(context.success?'Yay!':`Error: '${context.message}'`);}returncontext.result;}constisErrBoom=(error)=>errorinstanceofError&&error.message==='Boom';constisErrNotTrue=(error)=>errorinstanceofError&&error.message==="Not 'true'";constreturnFalse=(_param)=>false;constisTrue=(value)=>typeofvalue==='boolean'&&value;constisFalse=(value)=>typeofvalue==='boolean'&&!value;constnegate=(value)=>!value;constnegateOnlyTrue=(value)=>isTrue(value)?Result.ok(false):Result.err(newError("Not 'true'"));// hide try … catch inside `doSomething()` to produce a `Result`constresult1=doSomething(false);// 'Yay'console.assert(result1.isOk,'Should have been OK');console.assert(result1.mapOr(false,isTrue),"Ok value isn't 'true'");console.assert(result1.map(negate).mapOr(false,isFalse),"'map(negate)' didn't produce 'Ok(false)'");console.assert(result1.andThen(negateOnlyTrue).mapOr(false,isFalse),"'andThen(negateOnlyTrue)' didn't produce 'Ok(false)'");console.assert(result1.map(negate).andThen(negateOnlyTrue).mapOrElse(isErrNotTrue,returnFalse),"'andThen(negateOnlyTrue)' didn't produce 'Error(\"Not 'true'\")");constresult2=doSomething(true);// "Error: 'Boom'"console.assert(result2.isErr,'Should have been Error');console.assert(result2.mapOrElse(isErrBoom,returnFalse),"Message isn't 'Boom'");console.assert(result2.map(negate).mapOrElse(isErrBoom,returnFalse),"'map(negate)' didn't preserve Error");console.assert(result2.andThen(negateOnlyTrue).mapOrElse(isErrBoom,returnFalse),"'andThen(negateOnlyTrue)' didn't preserve Error");
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
All approaches have their trade-offs.
The Zen of Go:
Go's errors are values philosophy is a recognized pain point.
Handling errors where they occur - midstream - obscures, in use case terminology, the happy path/basic flow/main flow/main success scenario. That doesn't mean that other flows/scenarios aren't important - on the contrary. That's why there are extensions/alternate flows/recovery flows/exception flows/option flows.
Clearly identifying the main flow in code is valuable.
Whether or not an IDE can collapse all the conditional error handling is beside the point - especially given that traditionally Java has been chastised for needing an IDE to compensate for all its warts.
Granted Go doesn't use a containing tuple like
[error, result]
but destructuring still directly exposesnull
orundefined
values which is rather "un-functional" - typically the container (the tuple) is left as an opaque type while helper functions are used to work with the contained type indirectly (e.g. Rust's Result).Now your criticism regarding
try … catch
is fair …… but JavaScript isn't a functional language (which typically is immutable by default and supports the persistent data structures to make that sustainable). However it is possible to take inspiration from Rust:
Rust: A unique perspective
e.g. for local variables with exclusive/unique access (i.e. aren't somehow shared via a closure) mutation can be acceptable.
So maintaining a
context
object to "namespace" all the values that need to stretch across the varioustry … catch
scopes isn't too unreasonable: