{"record":{"id":"fdb625b54fbff960","repo":"airbnb/lottie-android","slug":"cannot-nest-start-calls-on-a-single-offscreenbit","errorCode":null,"errorMessage":"Cannot nest start() calls on a single OffscreenBitmap - call finish() first","messagePattern":"Cannot nest start\\(\\) calls on a single OffscreenBitmap - call finish\\(\\) first","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java","lineNumber":216,"sourceCode":"  }\n\n  private boolean needNewBitmap(@Nullable Bitmap bitmap, RectF bounds) {\n    if (bitmap == null) {\n      return true;\n    }\n\n    if (bounds.width() >= bitmap.getWidth() || bounds.height() >= bitmap.getHeight()) {\n      return true;\n    }\n\n    // If the required area has reduced in size considerably, trigger a reallocation, since\n    // we might be paying a large unnecessary penalty to work with a bitmap that big.\n    return bounds.width() < bitmap.getWidth() * 0.75f || bounds.height() < bitmap.getHeight() * 0.75f;\n  }\n\n  public Canvas start(Canvas parentCanvas, RectF bounds, ComposeOp op) {\n    if (this.parentCanvas != null) {\n      throw new IllegalStateException(\"Cannot nest start() calls on a single OffscreenBitmap - call finish() first\");\n    }\n\n    // Determine the scaling applied by the parentCanvas' pre-existing transform matrix. This is an optimization\n    // to avoid creating bitmaps (or render nodes) with unreasonable sizes that will get scaled down when drawn\n    // onto parentCanvas anyhow.\n    if (preExistingTransform == null) preExistingTransform = new float[9];\n    if (parentCanvasMatrix == null) parentCanvasMatrix = new Matrix();\n    parentCanvas.getMatrix(parentCanvasMatrix);\n    parentCanvasMatrix.getValues(preExistingTransform);\n\n    float pixelScaleX = preExistingTransform[Matrix.MSCALE_X];\n    float pixelScaleY = preExistingTransform[Matrix.MSCALE_Y];\n\n    if (scaledBounds == null) scaledBounds = new RectF();\n    scaledBounds.set(\n        bounds.left * pixelScaleX,\n        bounds.top * pixelScaleY,\n        bounds.right * pixelScaleX,","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/airbnb/lottie-android/blob/05ea92e90381eb8a8ae06855ea2b74f322bebbec/lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java#L198-L234","documentation":"Thrown by OffscreenLayer.start() when the method is called while a previous start() session is still active (parentCanvas != null at line 215). OffscreenLayer is a single-use-per-session offscreen rendering buffer — calling start() twice without calling finish() in between would corrupt the internal canvas state, transform matrix, and bitmap allocation tracking.","triggerScenarios":"Calling offscreenLayer.start(canvas, bounds, op) when a previous start() has not been matched by a finish(). The field this.parentCanvas is non-null from the prior start(), triggering the throw at line 216. This typically occurs in custom drawing code that draws multiple layers but forgot to finish one before starting the next, or in exception paths where finish() was skipped due to an earlier error.","commonSituations":"Custom Lottie drawable rendering code that nests offscreen layers incorrectly. An exception in the drawing logic between start() and finish() that prevented finish() from being called, leaving the layer in an active state. Reusing a single OffscreenLayer instance across multiple draw operations where the lifecycle is not properly managed. Recursive drawing that attempts to reuse the same layer.","solutions":["Ensure every start() call is paired with a finish() in a try/finally block so finish() runs even if drawing throws","If you need nested offscreen rendering, use a separate OffscreenLayer instance for each nesting level","Audit custom drawing code for all code paths between start() and finish() to guarantee finish() is always reached"],"exampleFix":"// before:\n// layer.start(canvas, bounds, op);\n// drawStuff(layer.getCanvas());  // if this throws, finish() is skipped\n// layer.finish();\n// layer.start(canvas, bounds2, op);  // throws: still active\n\n// after: use try/finally to guarantee finish()\nlayer.start(canvas, bounds, op);\ntry {\n  drawStuff(layer.getCanvas());\n} finally {\n  layer.finish();\n}\nlayer.start(canvas, bounds2, op);  // now safe","handlingStrategy":"validation","validationCode":"// Track session state to prevent nested start()\nprivate boolean offscreenActive = false;\n\nCanvas safeStart(OffscreenLayer layer, Canvas canvas, RectF bounds, ComposeOp op) {\n  if (offscreenActive) {\n    throw new IllegalStateException(\"OffscreenLayer already active\");\n  }\n  Canvas result = layer.start(canvas, bounds, op);\n  offscreenActive = true;\n  return result;\n}\n\nvoid safeFinish(OffscreenLayer layer) {\n  if (offscreenActive) {\n    layer.finish();\n    offscreenActive = false;\n  }\n}","typeGuard":null,"tryCatchPattern":"// Always pair start() and finish() with try/finally\nlayer.start(canvas, bounds, op);\ntry {\n  drawContent(childCanvas);\n} finally {\n  layer.finish(); // guaranteed to run\n}","preventionTips":["Always wrap start()/finish() in try/finally to guarantee cleanup","Use a separate OffscreenLayer instance for each nesting level","Track session state with a boolean flag if reusing the same instance"],"tags":["lottie","rendering","lifecycle","offscreen-bitmap"],"backgroundTag":null,"analyzedSha":"05ea92e90381eb8a8ae06855ea2b74f322bebbec","analyzedAt":"2026-08-14T00:51:35.636Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}