{"record":{"id":"0e712a776e0fe4e6","repo":"airbnb/lottie-android","slug":"offscreenbitmap-finish-call-without-matching-st","errorCode":null,"errorMessage":"OffscreenBitmap: finish() call without matching start()","messagePattern":"OffscreenBitmap: finish\\(\\) call without matching start\\(\\)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java","lineNumber":351,"sourceCode":"        renderNode.setHasOverlappingRendering(true);\n        renderNode.setPosition((int)scaledBounds.left, (int)scaledBounds.top, (int)scaledBounds.right, (int)scaledBounds.bottom);\n\n        childCanvas = renderNode.beginRecording((int) scaledBounds.width(), (int) scaledBounds.height());\n        childCanvas.setMatrix(OffscreenLayer.IDENTITY_MATRIX);\n        childCanvas.scale(pixelScaleX, pixelScaleY); // Replicate scaling applied by parentCanvas\n        childCanvas.translate(-bounds.left, -bounds.top); // So that the image begins at the top-left of the bitmap\n        break;\n\n      default:\n        throw new RuntimeException(\"Invalid render strategy for OffscreenLayer\");\n    }\n\n    return childCanvas;\n  }\n\n  public void finish() {\n    if (parentCanvas == null || op == null || preExistingTransform == null || targetRect == null) {\n      throw new IllegalStateException(\"OffscreenBitmap: finish() call without matching start()\");\n    }\n\n    switch (currentStrategy) {\n      case DIRECT:\n        parentCanvas.restore();\n        break;\n\n      case SAVE_LAYER:\n        parentCanvas.restore();\n        break;\n\n      case BITMAP:\n        if (bitmap == null) {\n          throw new IllegalStateException(\"Bitmap is not ready; should've been initialized at start() time\");\n        }\n\n        if (op.hasShadow()) {\n          // Composing the shadow first and then the content like this will be incorrect in the","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/airbnb/lottie-android/blob/05ea92e90381eb8a8ae06855ea2b74f322bebbec/lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java#L333-L369","documentation":"Thrown by OffscreenLayer.finish() when any of the internal session fields (parentCanvas, op, preExistingTransform, targetRect) are null at line 350. This means finish() was called without a prior successful start(), or the start() did not complete initialization. The finish() method expects a fully initialized session and cannot safely clean up a partial or nonexistent one.","triggerScenarios":"Calling offscreenLayer.finish() without having called start() first, or calling finish() twice (the second call finds parentCanvas null because the first finish() cleared it at line 405). Also possible if start() itself failed partway through initialization due to an exception, leaving fields partially set. The throw is at line 351.","commonSituations":"A double finish() call — finish() is called, clears parentCanvas (line 405), then finish() is called again in a finally block or cleanup path. Calling finish() in a finally block when start() was never called (e.g., start() was conditionally skipped). Cleanup code that unconditionally calls finish() regardless of whether start() succeeded. Exception in start() before all fields are set.","solutions":["Track the start/finish state with a boolean flag and only call finish() if start() succeeded and hasn't been finished yet","Ensure finish() is called exactly once per start() — use a try/finally where start() is outside the try, or guard with a flag","If using try/finally, structure it so finish() is only called when start() completed: set a boolean after successful start()"],"exampleFix":"// before:\n// try {\n//   layer.start(canvas, bounds, op);\n//   draw(layer.getCanvas());\n// } finally {\n//   layer.finish();  // called even if start() threw, or called twice\n// }\n\n// after: guard finish() with a state flag\nboolean started = false;\ntry {\n  layer.start(canvas, bounds, op);\n  started = true;\n  draw(layer.getCanvas());\n} finally {\n  if (started) {\n    layer.finish();\n  }\n}","handlingStrategy":"validation","validationCode":"// Guard finish() against missing or double calls\nboolean started = false;\n\nCanvas safeStart(OffscreenLayer layer, Canvas canvas, RectF bounds, ComposeOp op) {\n  Canvas result = layer.start(canvas, bounds, op);\n  started = true;\n  return result;\n}\n\nvoid safeFinish(OffscreenLayer layer) {\n  if (!started) return; // no-op if start() was never called or already finished\n  layer.finish();\n  started = false;\n}","typeGuard":null,"tryCatchPattern":"// Guarded try/finally pattern\nboolean started = false;\ntry {\n  layer.start(canvas, bounds, op);\n  started = true;\n  drawContent(layer.getCanvas());\n} finally {\n  if (started) layer.finish();\n}","preventionTips":["Use a boolean flag to track whether start() succeeded before calling finish()","Never call finish() twice — the first call clears parentCanvas at line 405","Structure try/finally so finish() is only reached when start() completed"],"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"}