Best way to assign value in a try block
Asked Answered
T

6

21
let x;
try {
  x = ...;
} catch (e) { 
  return
}

// rest of the code that uses `x`
const y = x + ...;

x is assigned exactly once, yet I have to use let instead of const.

The other way would be:

try {
  const x = ...;
  // rest of the code that uses `x`
  const y = x + ...;
} catch (e) { 
  return
}

However, this increases nesting and makes it unclear what can throw an error.

Is there a better way?

I don't have to care about the value of x if the try fails, as I'll be returning in the catch block.
I also don't want to extract it out to separate functions.

Tappet answered 12/5, 2018 at 9:41 Comment(14)
These are two different ways. This depends on how y is used. Your code doesn't show that.Quintile
What elaboration on the usage are you looking for? y could be used in several subsequent statements, eg const z = y + ....Tappet
If x was created from an awaited promise, you could use .then() for thisPreconceive
This is about how errors in the code that uses y should be handled. In second example the error in y code will be suppressed. In first example it will be thrown, so it could be handled. This depends on what you want.Quintile
@estus If the rest of the code could throw an error, I'll wrap them in a separate try so that I can handle them differently in the catch.Tappet
const is less useful in imperative style, because rebindings are common. So don't fight the style you program in, but just use let. Alternatively, you can switch to the functional style. Then const will be your friend.Regnant
@ftor how can I switch to "functional style" in this case?Tappet
You can't just switch. Using an error type in FP requires experience and prior knowledge. Besides, Javascript doesn't help you much, because most of its ecosystem is geared towards imperative programming.Regnant
"Using an error type in FP requires experience and prior knowledge" what does that mean? "experience and prior knowledge"? Your advise to "switch to the functional style" is followed by "you can't just switch"?Tappet
Sorry, my FP advice was a bit sloppy and meant in the long term. You can't just switch the paradigm, because you need to get familiar with the new one. This requires time. The learning curve is quite steep. My short term advice is: Stick to let.Regnant
Maybe you can explain how the above code can be converted into FP style? I don't think my presumed lack of experience with FP is relevant to how it can be applied in this context.Tappet
Use an option (aka maybe) type. This is a tagged union which represents either a value produced by a computation, or nothing (a computation without a result). The latter case short circuits the computation.Regnant
Can you post an answer with some brief javascript code snippets?Tappet
Some languages, like Scala, allow the value of the 'try' block to be passed to the outside. This eliminates the issue, without needing to do FP humdrums. Unfortunately, EcmaScript 'try' seems to be non-value-passing.Subulate
V
1

Whenever I run across something like this, I use a function:

function constTryCatch(valueFn, catchFn) {
  try {
    return valueFn();
  } catch (e) {
    if (catchFn) catchFn(e);
    return null;
  }
}

const obj = { foo: 'bar' };
const x = constTryCatch(() => obj.foo);
console.log(x);
const y = constTryCatch(() => obj.foo.bar.baz, (e) => console.log(e));
console.log(y);
// example, if the rest of the block depends on `y` being truthy:
// if (!y) return;

Note, the stack snippet doesn't display the error properly. In the real browser console, you'll see something like this:

bar

TypeError: Cannot read property 'baz' of undefined at constTryCatch ((index):79) at constTryCatch ((index):69) at window.onload ((index):79)

null

Vasilikivasilis answered 12/5, 2018 at 9:46 Comment(3)
This doesn't abort (return from the whole code) though in the catch case, it does assign null to x and continues.Preconceive
That's what the if (!y) return; line is for.Vasilikivasilis
Ah, hadn't noticed that. However, it checks only whether y is truthy, not whether no error had been thrown. If I want the code to work with arbitrary values for .baz (including falsy ones), I need an extra flag.Preconceive
P
1

Go functional - use a helper function with three callbacks:

function Try(attempt, onSuccess, onFailure) {
  try {
    var res = attempt();
  } catch(err) {
    return onFailure(err);
  }
  return onSuccess(res);
}

This allows you to write

return Try(() => …, x => {
  // rest of the code that uses `x`
  const y = x + …;
}, e => void e);

You can also make use of a data structure representing this control flow, like the Result monad (also known as the Either monad):

class Result {
  constructor(go) {
    this.go = go;
  }
  static Ok(v) {
    return new this((onSuccess, _) => onSuccess(v));
  }
  static Err(r) {
    return new this((_, onFailure) => onFailure(v));
  }
  map(f) {
    return this.go(v => Result.Ok(f(v)), r => Result.Err(r));
  }
  chain(f) {
    return this.go(v => f(v), r => Result.Err(r));
  }
  unwrap() {
    return this.go(v => v, r => { throw r; });
  }
}
function Try(attempt) {
  try {
    var res = attempt();
    return Result.Ok(res);
  } catch(e) {
    return Result.Err(e);
  }
}

You could use it very similar to the above simple helper function:

return Try(() =>
  … // exceptions in here are caught
).go(x => {
  // rest of the code that uses `x` - exceptions are not caught
  const y = x + …;
}, e => void e);

But also with more advanced chaining:

return Try(() =>
  … // exceptions in here are caught
).chain(x =>
  Try(() =>
    x + … // exceptions in here are caught as well
  )
).map(y =>
  … // exceptions in here are not caught
).unwrap(); // any caught exceptions are re-thrown
Preconceive answered 12/5, 2018 at 14:0 Comment(0)
I
1

I had the same problem today. I don't like the idea of creating new utility functions to work around it. Too much abstraction makes code hard for your team mates to understand.

I did think of one other possibility which would prevent accidental reassignment of x (but it is quite nasty).

let maybeX;
try {
  maybeX = ...;
} catch (e) {
  // do error handling 
  return;
}
const x = maybeX;

// rest of the code that uses `x`
const y = x + ...;
Indefatigable answered 4/6 at 14:24 Comment(0)
R
0

I would extract simple function to do the logic and error handling:

function getXOrHandleError() {
  try {
    return ...
  } catch (e) {
    // handle error
    return
  }
}

const x = getXOrHandleError()
// ...
Retread answered 12/5, 2018 at 10:8 Comment(0)
F
0

Using some functional standard library like SanctuaryJS you might rewrite your code using Either monad as follows:

// encaseEither wraps a function so it handles a throw and
// gets the Error object and builds a Left value.
const parseJson = S.encaseEither ( S.I ) ( JSON.parse )

// This will end up in Right, because given JSON
// could be parsed.
const eitherJson = parseJson ( '[ 1, 2, 3 ]' )

// However, this will end up in Left, because given input
// isn't a valid JSON
const eitherJson2 = parseJson ( '{' )

// Sets [ 1, 2, 3 ]
const result = S.fromEither ( [] ) ( eitherJson )

// Sets an empty array
const result2 = S.fromEither ( [] ) ( eitherJson2 )

BTW, if you're calling your own functions, you should avoid throw as much as possible and use Maybe or Either:

const someFun = x => x < 5 ? S.Nothing : S.Just ( x * 2 )

// Sets Just ( 20 )
const maybeResult = someFun ( 10 )

// Sets  Nothing
const maybeResult2 = someFun ( 3 ) 

const sumFun2 = x => x * 3

// Just ( 30 )
const maybeResult3 = S.map ( somFun2 ) ( maybeResult )

// Nothing
const maybeResult4 = S.map ( someFun2 ) ( maybeResult2 )

// Get maybeResult3 or output a default
// So it sets 30
const result3 = S.fromMaybe ( 0 ) ( maybeResult3 )

// Get maybeResult3 or output a default
// So it sets 0
const result4 = S.fromMaybe ( 0 ) ( maybeResult4 )

I would completely avoid exceptions. And for those functions from libraries and frameworks, I would encase them all to express effects using monads.

Fidellas answered 14/5, 2018 at 8:45 Comment(0)
Q
-1

The way this should be done depends on how errors should be handled. If they should be handled differently and they can be conditionally handled, there can be a single try...catch {

try {
  const x = ...;
  // rest of the code that uses `x`
  const y = x + ...;
  ...
} catch (e) { 
  if (e ...)
    return;
  else if (e ...)
    ...
  else
    ...
}

Alternatively, errors can be handled in caller function.

If errors cannot be conditionally handled, then there should be several try...catch:

let x;
try {
  x = ...;
} catch (e) { 
  return;
}

// rest of the code that uses `x`
try {
  const y = x + ...;
  ...
} catch (e) { 
  ...
}

Since x is block scope variable and needs to be used in another scope, it needs to be declared with let in parent scope.

Another way is to nest try...catch, this allows to prevent multiple assignments of x but increases nesting level:

try {
  const x = ...;

  // rest of the code that uses `x`
  try {
   const y = x + ...;
    ...
  } catch (e) { 
    ...
  }
} catch (e) { 
  return;
}
Quintile answered 12/5, 2018 at 10:0 Comment(2)
It can't be conditionally handled (the catch handles it based on which function throws the error rather than what error is thrown). I'm using the second approach, just didn't find it clean as let allows it to be reassigned later on even though the intend is to reassign it exactly once 2 lines below it.Tappet
Yes, reassignments is a problem with let. An alternative is var which is generally unfavourable because it allows to assign it on same line but allows reassignments and leaks the variable to enclosing scope. I updated the answer with another alternative. I generally won't go with nested try..catch unless I have real problems with unwanted reassignments or uncaught errors.Quintile

© 2022 - 2024 — McMap. All rights reserved.